Initial commit: Uruk Prize Platform architecture, backend core, mobile app, gateway caller & deployment pipeline
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Request;
|
||||
use Core\Response;
|
||||
use Core\Database;
|
||||
use PDO;
|
||||
|
||||
class GatewayController
|
||||
{
|
||||
private string $configuredAppKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->configuredAppKey = getenv('GATEWAY_APP_KEY') ?: 'uruk_gateway_secret_2026';
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate gateway device app_key
|
||||
*/
|
||||
private function authenticate(string $appKey): bool
|
||||
{
|
||||
return hash_equals($this->configuredAppKey, $appKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/gateway/pending-call or /pending-call.php
|
||||
*/
|
||||
public function pendingCall(Request $request): void
|
||||
{
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$appKey = (string)$request->get('app_key');
|
||||
|
||||
if (!$this->authenticate($appKey) || empty($deviceId)) {
|
||||
Response::json([
|
||||
'task_id' => null,
|
||||
'phone' => null,
|
||||
'caller_id' => null,
|
||||
'otp' => null,
|
||||
'timeout_seconds' => null,
|
||||
'error' => 'Unauthorized or missing device_id'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
|
||||
// Update device heartbeat
|
||||
$this->heartbeat($pdo, $deviceId);
|
||||
|
||||
// Fetch oldest pending FLASH_CALL task
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT id, target_phone, otp_code, timeout_seconds
|
||||
FROM gateway_tasks
|
||||
WHERE task_type = "FLASH_CALL"
|
||||
AND (assigned_device_id = :dev OR assigned_device_id IS NULL)
|
||||
AND status = "PENDING"
|
||||
AND created_at >= NOW() - INTERVAL 60 SECOND
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':dev' => $deviceId]);
|
||||
$task = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($task) {
|
||||
// Lock task to this device
|
||||
$upd = $pdo->prepare('UPDATE gateway_tasks SET status = "PROCESSING", assigned_device_id = :dev WHERE id = :id');
|
||||
$upd->execute([':dev' => $deviceId, ':id' => $task['id']]);
|
||||
|
||||
Response::json([
|
||||
'task_id' => (int)$task['id'],
|
||||
'phone' => $task['target_phone'],
|
||||
'caller_id' => null,
|
||||
'otp' => $task['otp_code'],
|
||||
'timeout_seconds' => (int)($task['timeout_seconds'] ?: 25),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'task_id' => null,
|
||||
'phone' => null,
|
||||
'caller_id' => null,
|
||||
'otp' => null,
|
||||
'timeout_seconds' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/gateway/pending-sms or /pending-sms.php
|
||||
*/
|
||||
public function pendingSms(Request $request): void
|
||||
{
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$appKey = (string)$request->get('app_key');
|
||||
|
||||
if (!$this->authenticate($appKey) || empty($deviceId)) {
|
||||
Response::json([
|
||||
'task_id' => null,
|
||||
'phone' => null,
|
||||
'caller_id' => null,
|
||||
'otp' => null,
|
||||
'timeout_seconds' => null,
|
||||
'error' => 'Unauthorized or missing device_id'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$this->heartbeat($pdo, $deviceId);
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT id, target_phone, otp_code, timeout_seconds
|
||||
FROM gateway_tasks
|
||||
WHERE task_type = "SMS"
|
||||
AND (assigned_device_id = :dev OR assigned_device_id IS NULL)
|
||||
AND status = "PENDING"
|
||||
AND created_at >= NOW() - INTERVAL 120 SECOND
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
');
|
||||
$stmt->execute([':dev' => $deviceId]);
|
||||
$task = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($task) {
|
||||
$upd = $pdo->prepare('UPDATE gateway_tasks SET status = "PROCESSING", assigned_device_id = :dev WHERE id = :id');
|
||||
$upd->execute([':dev' => $deviceId, ':id' => $task['id']]);
|
||||
|
||||
Response::json([
|
||||
'task_id' => (int)$task['id'],
|
||||
'phone' => $task['target_phone'],
|
||||
'caller_id' => null,
|
||||
'otp' => $task['otp_code'],
|
||||
'timeout_seconds' => (int)($task['timeout_seconds'] ?: 30),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'task_id' => null,
|
||||
'phone' => null,
|
||||
'caller_id' => null,
|
||||
'otp' => null,
|
||||
'timeout_seconds' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/gateway/call-done or /call-done.php
|
||||
*/
|
||||
public function callDone(Request $request): void
|
||||
{
|
||||
$taskId = (int)$request->get('task_id');
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$appKey = (string)$request->get('app_key');
|
||||
$result = (string)$request->get('result');
|
||||
|
||||
if (!$this->authenticate($appKey)) {
|
||||
Response::json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$newStatus = (strtoupper($result) === 'SUCCESS' || strtoupper($result) === 'DONE') ? 'COMPLETED' : 'FAILED';
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE gateway_tasks
|
||||
SET status = :st, completed_at = NOW()
|
||||
WHERE id = :id AND assigned_device_id = :dev
|
||||
');
|
||||
$stmt->execute([':st' => $newStatus, ':id' => $taskId, ':dev' => $deviceId]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => "Call task {$taskId} recorded as {$newStatus}.",
|
||||
'device_id' => $deviceId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/gateway/sms-done or /sms-done.php
|
||||
*/
|
||||
public function smsDone(Request $request): void
|
||||
{
|
||||
$taskId = (int)$request->get('task_id');
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$appKey = (string)$request->get('app_key');
|
||||
$result = (string)$request->get('result');
|
||||
|
||||
if (!$this->authenticate($appKey)) {
|
||||
Response::json(['success' => false, 'message' => 'Unauthorized'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$newStatus = (strtoupper($result) === 'SUCCESS' || strtoupper($result) === 'SENT') ? 'COMPLETED' : 'FAILED';
|
||||
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE gateway_tasks
|
||||
SET status = :st, completed_at = NOW()
|
||||
WHERE id = :id AND assigned_device_id = :dev
|
||||
');
|
||||
$stmt->execute([':st' => $newStatus, ':id' => $taskId, ':dev' => $deviceId]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => "SMS task {$taskId} recorded as {$newStatus}.",
|
||||
'device_id' => $deviceId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/gateway/register-device or /register-device.php
|
||||
*/
|
||||
public function registerDevice(Request $request): void
|
||||
{
|
||||
$deviceId = (string)$request->get('device_id');
|
||||
$phoneNumber = (string)$request->get('phone_number');
|
||||
$simSlot = (int)$request->get('sim_slot', 1);
|
||||
$appKey = (string)$request->get('app_key');
|
||||
|
||||
if (!$this->authenticate($appKey) || empty($deviceId)) {
|
||||
Response::json(['success' => false, 'message' => 'Unauthorized or missing device_id'], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = Database::getConnection();
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO gateway_devices (device_id, phone_number, sim_slot, status, last_heartbeat)
|
||||
VALUES (:did, :phone, :slot, "ACTIVE", NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
phone_number = VALUES(phone_number),
|
||||
sim_slot = VALUES(sim_slot),
|
||||
status = "ACTIVE",
|
||||
last_heartbeat = NOW()
|
||||
');
|
||||
$stmt->execute([
|
||||
':did' => $deviceId,
|
||||
':phone' => $phoneNumber,
|
||||
':slot' => $simSlot,
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'success' => true,
|
||||
'message' => 'Gateway device registered and active.',
|
||||
'device_id' => $deviceId,
|
||||
]);
|
||||
}
|
||||
|
||||
private function heartbeat(PDO $pdo, string $deviceId): void
|
||||
{
|
||||
try {
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO gateway_devices (device_id, status, last_heartbeat)
|
||||
VALUES (:did, "ACTIVE", NOW())
|
||||
ON DUPLICATE KEY UPDATE status = "ACTIVE", last_heartbeat = NOW()
|
||||
');
|
||||
$stmt->execute([':did' => $deviceId]);
|
||||
} catch (\Throwable $e) {
|
||||
// Ignore heartbeat failures if table schema is pending
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user