Add Nabeh integration: nabeh/ endpoints with NABEH_API_KEY auth
This commit is contained in:
@@ -102,6 +102,14 @@ MAIL_PASS=<CHANGE_ME_EMAIL_PASSWORD>
|
|||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
APP_DEBUG=false
|
APP_DEBUG=false
|
||||||
APP_NAME=Siro
|
APP_NAME=Siro
|
||||||
|
APP_DOMAIN=api-syria.siromove.com
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Nabeh Integration (server-to-server API key)
|
||||||
|
# Must match NABEH_API_KEY in Nabeh's .env
|
||||||
|
# =============================================================================
|
||||||
|
NABEH_API_KEY=<CHANGE_ME_SHARED_SECRET>
|
||||||
|
SECRET_KEY_HMAC=<CHANGE_ME_HMAC_SECRET_FOR_SIGNED_URLS>
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Security Configuration - Fingerprint
|
# Security Configuration - Fingerprint
|
||||||
|
|||||||
91
backend/nabeh/driver_status.php
Normal file
91
backend/nabeh/driver_status.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Nabeh Integration — Driver Status Check
|
||||||
|
*
|
||||||
|
* Called by Nabeh AI platform to check driver registration/activation status.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
|
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||||
|
|
||||||
|
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||||
|
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||||
|
|
||||||
|
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phone = $_GET['phone'] ?? '';
|
||||||
|
|
||||||
|
if (empty($phone)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Phone parameter required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::get('main');
|
||||||
|
global $encryptionHelper;
|
||||||
|
|
||||||
|
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT d.id, d.phone, d.first_name, d.last_name, d.status, d.created_at,
|
||||||
|
cr.id as car_id, cr.make, cr.model, cr.year, cr.car_plate, cr.status as car_status
|
||||||
|
FROM driver d
|
||||||
|
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
|
||||||
|
WHERE d.phone = :phone
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':phone' => $encryptedPhone,
|
||||||
|
]);
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => null,
|
||||||
|
'message' => 'Driver not found'
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decryptedPhone = $encryptionHelper->decryptData($result['phone']);
|
||||||
|
$decryptedFirstName = $encryptionHelper->decryptData($result['first_name']);
|
||||||
|
$decryptedLastName = $encryptionHelper->decryptData($result['last_name']);
|
||||||
|
|
||||||
|
$docStmt = $db->prepare("SELECT doc_type, link FROM driver_documents WHERE driverID = :driverID");
|
||||||
|
$docStmt->execute([':driverID' => $result['id']]);
|
||||||
|
$documents = $docStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => [
|
||||||
|
'driver_id' => $result['id'],
|
||||||
|
'phone' => $decryptedPhone,
|
||||||
|
'name' => trim($decryptedFirstName . ' ' . $decryptedLastName),
|
||||||
|
'status' => $result['status'],
|
||||||
|
'registered_at' => $result['created_at'],
|
||||||
|
'car' => [
|
||||||
|
'id' => $result['car_id'],
|
||||||
|
'make' => $result['make'],
|
||||||
|
'model' => $result['model'],
|
||||||
|
'year' => $result['year'],
|
||||||
|
'plate' => $result['car_plate'],
|
||||||
|
'status' => $result['car_status'],
|
||||||
|
],
|
||||||
|
'documents' => $documents,
|
||||||
|
]
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("[Nabeh Status Error] " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Internal server error']);
|
||||||
|
}
|
||||||
252
backend/nabeh/query.php
Normal file
252
backend/nabeh/query.php
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Nabeh Integration — Unified Query API
|
||||||
|
*
|
||||||
|
* Called by Nabeh AI platform to query driver info, trips, stats, and trip details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
|
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||||
|
|
||||||
|
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||||
|
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||||
|
|
||||||
|
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized: invalid API key']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = [];
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
$raw = file_get_contents('php://input');
|
||||||
|
$input = json_decode($raw, true) ?: [];
|
||||||
|
} else {
|
||||||
|
$input = $_GET;
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryType = $input['query_type'] ?? '';
|
||||||
|
$phone = preg_replace('/[^0-9]/', '', $input['phone'] ?? '');
|
||||||
|
$driverId = $input['driver_id'] ?? '';
|
||||||
|
$tripId = $input['trip_id'] ?? '';
|
||||||
|
$limit = min((int)($input['limit'] ?? 10), 50);
|
||||||
|
|
||||||
|
if (empty($queryType)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'query_type is required. Options: driver_info, driver_trips, driver_stats, trip_detail']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validTypes = ['driver_info', 'driver_trips', 'driver_stats', 'trip_detail'];
|
||||||
|
if (!in_array($queryType, $validTypes, true)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Invalid query_type. Options: ' . implode(', ', $validTypes)]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
global $encryptionHelper;
|
||||||
|
$mainDb = Database::get('main');
|
||||||
|
$rideDb = Database::get('ride');
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
if ($queryType === 'driver_info') {
|
||||||
|
if (empty($phone)) {
|
||||||
|
jsonError('phone parameter is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||||
|
|
||||||
|
$stmt = $mainDb->prepare("
|
||||||
|
SELECT d.id, d.phone, d.first_name, d.last_name, d.name_arabic,
|
||||||
|
d.status, d.created_at, d.birthdate, d.gender, d.site,
|
||||||
|
cr.id as car_id, cr.make, cr.model, cr.year, cr.car_plate,
|
||||||
|
cr.color, cr.color_hex, cr.fuel, cr.vin,
|
||||||
|
cr.status as car_status, cr.expiration_date
|
||||||
|
FROM driver d
|
||||||
|
LEFT JOIN CarRegistration cr ON cr.driverID = d.id
|
||||||
|
WHERE d.phone = :phone OR d.email LIKE :phoneLike
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':phone' => $encryptedPhone,
|
||||||
|
':phoneLike' => $phone . '%',
|
||||||
|
]);
|
||||||
|
$driver = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$driver) {
|
||||||
|
echo json_encode(['status' => 'success', 'data' => null, 'message' => 'Driver not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decrypt = function($val) use ($encryptionHelper) {
|
||||||
|
return $val ? $encryptionHelper->decryptData($val) : $val;
|
||||||
|
};
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => [
|
||||||
|
'driver_id' => $driver['id'],
|
||||||
|
'phone' => $decrypt($driver['phone']),
|
||||||
|
'first_name' => $decrypt($driver['first_name']),
|
||||||
|
'last_name' => $decrypt($driver['last_name']),
|
||||||
|
'name_arabic' => $decrypt($driver['name_arabic']),
|
||||||
|
'gender' => $decrypt($driver['gender']),
|
||||||
|
'birthdate' => $driver['birthdate'],
|
||||||
|
'status' => $driver['status'],
|
||||||
|
'site' => $decrypt($driver['site']),
|
||||||
|
'registered_at' => $driver['created_at'],
|
||||||
|
'car' => $driver['car_id'] ? [
|
||||||
|
'id' => $driver['car_id'],
|
||||||
|
'make' => $driver['make'],
|
||||||
|
'model' => $driver['model'],
|
||||||
|
'year' => $driver['year'],
|
||||||
|
'plate' => $driver['car_plate'],
|
||||||
|
'color' => $driver['color'],
|
||||||
|
'color_hex' => $driver['color_hex'],
|
||||||
|
'fuel' => $driver['fuel'],
|
||||||
|
'vin' => $decrypt($driver['vin']),
|
||||||
|
'status' => $driver['car_status'],
|
||||||
|
'expiration_date' => $driver['expiration_date'],
|
||||||
|
] : null,
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
if ($queryType === 'driver_trips') {
|
||||||
|
if (empty($driverId) && empty($phone)) {
|
||||||
|
jsonError('driver_id or phone is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($driverId) && !empty($phone)) {
|
||||||
|
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||||
|
$stmt = $mainDb->prepare("SELECT id FROM driver WHERE phone = :phone LIMIT 1");
|
||||||
|
$stmt->execute([':phone' => $encryptedPhone]);
|
||||||
|
$driverRow = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$driverRow) {
|
||||||
|
echo json_encode(['status' => 'success', 'data' => [], 'message' => 'Driver not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$driverId = $driverRow['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $rideDb->prepare("
|
||||||
|
SELECT id, start_location, end_location, date, time, endtime,
|
||||||
|
price, price_for_driver, price_for_passenger,
|
||||||
|
status, paymentMethod, carType, distance, created_at
|
||||||
|
FROM ride
|
||||||
|
WHERE driver_id = :driver_id
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT :lim
|
||||||
|
");
|
||||||
|
$stmt->bindValue(':driver_id', $driverId, PDO::PARAM_STR);
|
||||||
|
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
$trips = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $trips,
|
||||||
|
'count' => count($trips),
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
if ($queryType === 'driver_stats') {
|
||||||
|
if (empty($driverId) && empty($phone)) {
|
||||||
|
jsonError('driver_id or phone is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($driverId) && !empty($phone)) {
|
||||||
|
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||||
|
$stmt = $mainDb->prepare("SELECT id FROM driver WHERE phone = :phone LIMIT 1");
|
||||||
|
$stmt->execute([':phone' => $encryptedPhone]);
|
||||||
|
$driverRow = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (!$driverRow) {
|
||||||
|
echo json_encode(['status' => 'success', 'data' => null, 'message' => 'Driver not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$driverId = $driverRow['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $rideDb->prepare("
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_trips,
|
||||||
|
COALESCE(SUM(price_for_driver), 0) as total_earnings,
|
||||||
|
COALESCE(SUM(price_for_passenger), 0) as total_collected,
|
||||||
|
COALESCE(AVG(price_for_driver), 0) as avg_earning_per_trip,
|
||||||
|
COALESCE(SUM(distance), 0) as total_distance,
|
||||||
|
COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_trips,
|
||||||
|
COUNT(CASE WHEN status = 'cancelled' THEN 1 END) as cancelled_trips
|
||||||
|
FROM ride
|
||||||
|
WHERE driver_id = :driver_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':driver_id' => $driverId]);
|
||||||
|
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$driverStmt = $mainDb->prepare("SELECT status, created_at FROM driver WHERE id = :id LIMIT 1");
|
||||||
|
$driverStmt->execute([':id' => $driverId]);
|
||||||
|
$driverStatus = $driverStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => [
|
||||||
|
'driver_id' => $driverId,
|
||||||
|
'status' => $driverStatus['status'] ?? 'unknown',
|
||||||
|
'registered_at' => $driverStatus['created_at'] ?? null,
|
||||||
|
'stats' => [
|
||||||
|
'total_trips' => (int)$stats['total_trips'],
|
||||||
|
'completed_trips' => (int)$stats['completed_trips'],
|
||||||
|
'cancelled_trips' => (int)$stats['cancelled_trips'],
|
||||||
|
'total_earnings' => (float)$stats['total_earnings'],
|
||||||
|
'total_collected' => (float)$stats['total_collected'],
|
||||||
|
'avg_earning_per_trip' => (float)$stats['avg_earning_per_trip'],
|
||||||
|
'total_distance_km' => (float)$stats['total_distance'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
if ($queryType === 'trip_detail') {
|
||||||
|
if (empty($tripId)) {
|
||||||
|
jsonError('trip_id is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $rideDb->prepare("
|
||||||
|
SELECT r.*,
|
||||||
|
p.first_name as passenger_first_name,
|
||||||
|
p.last_name as passenger_last_name,
|
||||||
|
p.phone as passenger_phone
|
||||||
|
FROM ride r
|
||||||
|
LEFT JOIN driver p ON p.id = r.passenger_id
|
||||||
|
WHERE r.id = :id
|
||||||
|
LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([':id' => $tripId]);
|
||||||
|
$trip = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$trip) {
|
||||||
|
echo json_encode(['status' => 'success', 'data' => null, 'message' => 'Trip not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'data' => $trip,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("[Nabeh Query Error] " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Internal server error']);
|
||||||
|
}
|
||||||
218
backend/nabeh/register.php
Normal file
218
backend/nabeh/register.php
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Nabeh Integration — Driver Registration
|
||||||
|
*
|
||||||
|
* Called by Nabeh AI platform to register drivers in Siro.
|
||||||
|
* Authenticated via NABEH_API_KEY (shared between servers).
|
||||||
|
* Handles all 3 countries: Syria, Jordan, Egypt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
|
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Method not allowed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||||
|
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||||
|
|
||||||
|
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized: invalid API key']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
$rateLimitFile = __DIR__ . '/../logs/nabeh_rate_' . md5($_SERVER['REMOTE_ADDR'] ?? 'unknown') . '.lock';
|
||||||
|
$rateLimitWindow = 60;
|
||||||
|
$rateLimitMax = 5;
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
$attempts = [];
|
||||||
|
if (file_exists($rateLimitFile)) {
|
||||||
|
$attempts = json_decode(file_get_contents($rateLimitFile), true) ?: [];
|
||||||
|
$attempts = array_filter($attempts, fn($t) => $t > ($now - $rateLimitWindow));
|
||||||
|
}
|
||||||
|
if (count($attempts) >= $rateLimitMax) {
|
||||||
|
http_response_code(429);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Too many requests. Try again later.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$attempts[] = $now;
|
||||||
|
file_put_contents($rateLimitFile, json_encode($attempts), LOCK_EX);
|
||||||
|
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (!$input) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Invalid JSON body']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$phone = preg_replace('/[^0-9]/', '', $input['phone'] ?? '');
|
||||||
|
$password = $input['password'] ?? substr(md5($phone . time()), 0, 12);
|
||||||
|
$firstName = $input['first_name'] ?? $input['name_arabic'] ?? '';
|
||||||
|
$lastName = $input['last_name'] ?? '-';
|
||||||
|
$nameArabic = $input['name_arabic'] ?? $firstName;
|
||||||
|
$nationalNumber = $input['national_number'] ?? '';
|
||||||
|
$birthdate = $input['birthdate'] ?? '';
|
||||||
|
$address = $input['address'] ?? '';
|
||||||
|
$gender = $input['gender'] ?? 'Male';
|
||||||
|
$site = $input['site'] ?? '';
|
||||||
|
|
||||||
|
$vin = $input['vin'] ?? '';
|
||||||
|
$carPlate = $input['car_plate'] ?? '';
|
||||||
|
$make = $input['make'] ?? '';
|
||||||
|
$model = $input['model'] ?? '';
|
||||||
|
$year = $input['year'] ?? '';
|
||||||
|
$color = $input['color'] ?? '';
|
||||||
|
$colorHex = $input['color_hex'] ?? '#000000';
|
||||||
|
$owner = $input['owner'] ?? '';
|
||||||
|
$fuel = $input['fuel'] ?? 'Petrol';
|
||||||
|
$expirationDate = $input['expiration_date'] ?? '';
|
||||||
|
|
||||||
|
$idFront = $input['id_front'] ?? $input['id_front_url'] ?? '';
|
||||||
|
$idBack = $input['id_back'] ?? $input['id_back_url'] ?? '';
|
||||||
|
$driverLicense = $input['driver_license'] ?? $input['driver_license_front_url'] ?? '';
|
||||||
|
$driverLicenseBack = $input['driver_license_back'] ?? $input['driver_license_back_url'] ?? '';
|
||||||
|
$carLicenseFront = $input['car_license_front'] ?? $input['vehicle_license_front_url'] ?? '';
|
||||||
|
$carLicenseBack = $input['car_license_back'] ?? $input['vehicle_license_back_url'] ?? '';
|
||||||
|
$criminalRecord = $input['criminal_record'] ?? $input['criminal_record_url'] ?? '';
|
||||||
|
$profilePicture = $input['profile_picture'] ?? '';
|
||||||
|
|
||||||
|
if (empty($phone)) {
|
||||||
|
jsonError('Phone number is required');
|
||||||
|
}
|
||||||
|
if (empty($firstName)) {
|
||||||
|
jsonError('First name is required');
|
||||||
|
}
|
||||||
|
if (empty($vin) || empty($carPlate) || empty($make) || empty($model) || empty($year)) {
|
||||||
|
jsonError('Car details (vin, plate, make, model, year) are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = Database::get('main');
|
||||||
|
$driverId = 'DRV' . date('YmdHis') . rand(100, 999);
|
||||||
|
$hashedPassword = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
|
||||||
|
|
||||||
|
global $encryptionHelper;
|
||||||
|
$encryptedPhone = $encryptionHelper->encryptData($phone);
|
||||||
|
$encryptedFirstName = $encryptionHelper->encryptData($firstName);
|
||||||
|
$encryptedLastName = $encryptionHelper->encryptData($lastName);
|
||||||
|
$encryptedNameArabic = $encryptionHelper->encryptData($nameArabic);
|
||||||
|
$encryptedNationalNumber = !empty($nationalNumber) ? $encryptionHelper->encryptData($nationalNumber) : '';
|
||||||
|
$encryptedAddress = !empty($address) ? $encryptionHelper->encryptData($address) : '';
|
||||||
|
$encryptedGender = $encryptionHelper->encryptData($gender);
|
||||||
|
$encryptedVin = $encryptionHelper->encryptData($vin);
|
||||||
|
$encryptedCarPlate = $encryptionHelper->encryptData($carPlate);
|
||||||
|
$encryptedOwner = $encryptionHelper->encryptData($owner);
|
||||||
|
$encryptedSite = !empty($site) ? $encryptionHelper->encryptData($site) : $encryptedAddress;
|
||||||
|
|
||||||
|
$nowDate = date('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$driverStmt = $db->prepare("
|
||||||
|
INSERT INTO driver (
|
||||||
|
id, phone, email, password, gender, first_name, last_name,
|
||||||
|
name_arabic, national_number, address, site, birthdate,
|
||||||
|
status, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
:id, :phone, :email, :password, :gender, :first_name, :last_name,
|
||||||
|
:name_arabic, :national_number, :address, :site, :birthdate,
|
||||||
|
'yet', :created_at, :updated_at
|
||||||
|
)
|
||||||
|
");
|
||||||
|
$driverStmt->execute([
|
||||||
|
':id' => $driverId,
|
||||||
|
':phone' => $encryptedPhone,
|
||||||
|
':email' => $phone . '@intaleqapp.com',
|
||||||
|
':password' => $hashedPassword,
|
||||||
|
':gender' => $encryptedGender,
|
||||||
|
':first_name' => $encryptedFirstName,
|
||||||
|
':last_name' => $encryptedLastName,
|
||||||
|
':name_arabic' => $encryptedNameArabic,
|
||||||
|
':national_number' => $encryptedNationalNumber,
|
||||||
|
':address' => $encryptedAddress,
|
||||||
|
':site' => $encryptedSite,
|
||||||
|
':birthdate' => $birthdate,
|
||||||
|
':created_at' => $nowDate,
|
||||||
|
':updated_at' => $nowDate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$carStmt = $db->prepare("
|
||||||
|
INSERT INTO CarRegistration (
|
||||||
|
driverID, vin, car_plate, make, model, year,
|
||||||
|
expiration_date, color, owner, color_hex, fuel,
|
||||||
|
isDefault, status, created_at, vehicle_category_id
|
||||||
|
) VALUES (
|
||||||
|
:driverID, :vin, :car_plate, :make, :model, :year,
|
||||||
|
:expiration_date, :color, :owner, :color_hex, :fuel,
|
||||||
|
1, 'yet', :created_at, 1
|
||||||
|
)
|
||||||
|
");
|
||||||
|
$carStmt->execute([
|
||||||
|
':driverID' => $driverId,
|
||||||
|
':vin' => $encryptedVin,
|
||||||
|
':car_plate' => $encryptedCarPlate,
|
||||||
|
':make' => $make,
|
||||||
|
':model' => $model,
|
||||||
|
':year' => $year,
|
||||||
|
':expiration_date' => $expirationDate ?: $nowDate,
|
||||||
|
':color' => $color,
|
||||||
|
':owner' => $encryptedOwner,
|
||||||
|
':color_hex' => $colorHex,
|
||||||
|
':fuel' => $fuel,
|
||||||
|
':created_at' => $nowDate,
|
||||||
|
]);
|
||||||
|
$carRegId = $db->lastInsertId();
|
||||||
|
|
||||||
|
$docTypes = [
|
||||||
|
'id_front' => $idFront,
|
||||||
|
'id_back' => $idBack,
|
||||||
|
'driver_license' => $driverLicense,
|
||||||
|
'driver_license_back' => $driverLicenseBack,
|
||||||
|
'car_license_front' => $carLicenseFront,
|
||||||
|
'car_license_back' => $carLicenseBack,
|
||||||
|
'criminal_record' => $criminalRecord,
|
||||||
|
'profile_picture' => $profilePicture,
|
||||||
|
];
|
||||||
|
|
||||||
|
$docStmt = $db->prepare("
|
||||||
|
INSERT INTO driver_documents (driverID, doc_type, image_name, link, upload_date)
|
||||||
|
VALUES (:driverID, :doc_type, :image_name, :link, :upload_date)
|
||||||
|
");
|
||||||
|
foreach ($docTypes as $docType => $link) {
|
||||||
|
if (!empty($link)) {
|
||||||
|
$docStmt->execute([
|
||||||
|
':driverID' => $driverId,
|
||||||
|
':doc_type' => $docType,
|
||||||
|
':image_name' => $driverId . '_' . $docType,
|
||||||
|
':link' => $link,
|
||||||
|
':upload_date' => $nowDate,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log("[Nabeh Registration] New driver registered: {$driverId}, Phone: {$phone}");
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'success',
|
||||||
|
'message' => [
|
||||||
|
'status' => 'success',
|
||||||
|
'driverID' => $driverId,
|
||||||
|
'carRegID' => $carRegId,
|
||||||
|
]
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("[Nabeh Registration Error] " . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'failure',
|
||||||
|
'message' => 'Internal server error: ' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
164
backend/nabeh/upload_document.php
Normal file
164
backend/nabeh/upload_document.php
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Nabeh Integration — Document Upload
|
||||||
|
*
|
||||||
|
* Called by Nabeh AI platform to upload driver documents to Siro's private storage.
|
||||||
|
* Returns a signed URL valid for 48 hours.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../core/bootstrap.php';
|
||||||
|
|
||||||
|
header('Access-Control-Allow-Origin: *');
|
||||||
|
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||||
|
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Method not allowed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
|
||||||
|
$expectedKey = getenv('NABEH_API_KEY') ?: '';
|
||||||
|
|
||||||
|
if (empty($apiKey) || $apiKey !== $expectedKey) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized: invalid API key']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
$rateLimitFile = __DIR__ . '/../logs/nabeh_upload_rate_' . md5($_SERVER['REMOTE_ADDR'] ?? 'unknown') . '.lock';
|
||||||
|
$rateLimitWindow = 60;
|
||||||
|
$rateLimitMax = 10;
|
||||||
|
|
||||||
|
$nowTime = time();
|
||||||
|
$attempts = [];
|
||||||
|
if (file_exists($rateLimitFile)) {
|
||||||
|
$attempts = json_decode(file_get_contents($rateLimitFile), true) ?: [];
|
||||||
|
$attempts = array_filter($attempts, fn($t) => $t > ($nowTime - $rateLimitWindow));
|
||||||
|
}
|
||||||
|
if (count($attempts) >= $rateLimitMax) {
|
||||||
|
http_response_code(429);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Too many requests. Try again later.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$attempts[] = $nowTime;
|
||||||
|
file_put_contents($rateLimitFile, json_encode($attempts), LOCK_EX);
|
||||||
|
|
||||||
|
const MAX_FILE_MB = 5;
|
||||||
|
const ALLOWED_MIMES = ['image/jpeg', 'image/png', 'image/webp'];
|
||||||
|
const UPLOAD_ROOT = __DIR__ . '/../private_uploads';
|
||||||
|
const SIGNED_TTL_SEC = 172800;
|
||||||
|
|
||||||
|
$signSecret = getenv('SECRET_KEY_HMAC') ?: '';
|
||||||
|
if (empty($signSecret)) {
|
||||||
|
uploadLog('[Nabeh Upload] SECRET_KEY_HMAC not configured', 'ERROR');
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['status' => 'failure', 'message' => 'Server configuration error']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = getenv('APP_DOMAIN') ?: 'api-syria.siromove.com';
|
||||||
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
|
define('PUBLIC_BASE', "$protocol://$host/siro");
|
||||||
|
|
||||||
|
if (!is_dir(UPLOAD_ROOT)) {
|
||||||
|
@mkdir(UPLOAD_ROOT, 0700, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadLog("[Nabeh Upload] Document upload started");
|
||||||
|
|
||||||
|
$allowedDocTypes = [
|
||||||
|
'id_front', 'id_back',
|
||||||
|
'driver_license_front', 'driver_license_back',
|
||||||
|
'car_license_front', 'car_license_back',
|
||||||
|
'criminal_record', 'profile_picture',
|
||||||
|
];
|
||||||
|
|
||||||
|
$driverId = $_POST['driver_id'] ?? '';
|
||||||
|
$docType = $_POST['doc_type'] ?? '';
|
||||||
|
|
||||||
|
if (empty($driverId) || empty($docType)) {
|
||||||
|
jsonError('driver_id and doc_type are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$driverIdSafe = preg_replace('/[^A-Za-z0-9_\-]/', '_', $driverId);
|
||||||
|
|
||||||
|
if (!in_array($docType, $allowedDocTypes, true)) {
|
||||||
|
jsonError("Invalid doc_type. Allowed: " . implode(', ', $allowedDocTypes));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$errCode = $_FILES['file']['error'] ?? 'missing_file';
|
||||||
|
uploadLog("[Nabeh Upload] File upload error. Code: $errCode", 'ERROR');
|
||||||
|
jsonError('No file uploaded or upload error.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmpPath = $_FILES['file']['tmp_name'];
|
||||||
|
$size = filesize($tmpPath);
|
||||||
|
if ($size === false || $size <= 0) {
|
||||||
|
jsonError('Invalid file size.');
|
||||||
|
}
|
||||||
|
if ($size > MAX_FILE_MB * 1024 * 1024) {
|
||||||
|
jsonError('File too large. Max ' . MAX_FILE_MB . ' MB.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||||
|
$mime = $finfo->file($tmpPath) ?: 'application/octet-stream';
|
||||||
|
if (!in_array($mime, ALLOWED_MIMES, true)) {
|
||||||
|
jsonError("Unsupported file type: $mime");
|
||||||
|
}
|
||||||
|
|
||||||
|
$extMap = [
|
||||||
|
'image/jpeg' => '.jpg',
|
||||||
|
'image/png' => '.png',
|
||||||
|
'image/webp' => '.webp',
|
||||||
|
];
|
||||||
|
$ext = $extMap[$mime];
|
||||||
|
|
||||||
|
$h = hash('sha1', $driverIdSafe);
|
||||||
|
$subdir = substr($h, 0, 2) . '/' . substr($h, 2, 2);
|
||||||
|
$destDir = UPLOAD_ROOT . '/' . $subdir;
|
||||||
|
if (!is_dir($destDir)) { @mkdir($destDir, 0700, true); }
|
||||||
|
|
||||||
|
$serverName = "{$driverIdSafe}__{$docType}{$ext}";
|
||||||
|
$destPath = $destDir . '/' . $serverName;
|
||||||
|
|
||||||
|
$resolvedDest = realpath($destPath) ?: $destPath;
|
||||||
|
$resolvedRoot = realpath(UPLOAD_ROOT) ?: UPLOAD_ROOT;
|
||||||
|
|
||||||
|
if (is_file($destPath) && str_starts_with($resolvedDest, $resolvedRoot)) {
|
||||||
|
@unlink($destPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!move_uploaded_file($tmpPath, $destPath)) {
|
||||||
|
jsonError('Failed to save the uploaded file.');
|
||||||
|
}
|
||||||
|
@chmod($destPath, 0600);
|
||||||
|
|
||||||
|
$extShort = ltrim($ext, '.');
|
||||||
|
$expires = time() + SIGNED_TTL_SEC;
|
||||||
|
$message = $driverIdSafe . ':' . $docType . ':' . $extShort . ':' . $expires;
|
||||||
|
$signature = hash_hmac('sha256', $message, $signSecret);
|
||||||
|
|
||||||
|
$fileUrl = PUBLIC_BASE . '/secure_image.php'
|
||||||
|
. '?driver_id=' . urlencode($driverIdSafe)
|
||||||
|
. '&doc_type=' . urlencode($docType)
|
||||||
|
. '&ext=' . urlencode($extShort)
|
||||||
|
. '&expires=' . $expires
|
||||||
|
. '&signature=' . urlencode($signature);
|
||||||
|
|
||||||
|
uploadLog("[Nabeh Upload] Document uploaded successfully. Type: $docType, Size: $size");
|
||||||
|
|
||||||
|
printSuccess([
|
||||||
|
'status' => 'success',
|
||||||
|
'success_file' => true,
|
||||||
|
'file_url' => $fileUrl,
|
||||||
|
'file_name' => $serverName,
|
||||||
|
'driver_id' => $driverIdSafe,
|
||||||
|
'doc_type' => $docType,
|
||||||
|
'mime_type' => $mime,
|
||||||
|
'size_bytes' => $size,
|
||||||
|
'expires_at' => date('c', $expires),
|
||||||
|
]);
|
||||||
Reference in New Issue
Block a user