www/api/index.php
2026-05-11 23:31:48 +01:00

203 lines
7.6 KiB
PHP

<?php declare(strict_types=1);
require __DIR__.'/bootstrap.php';
$pdo = db();
function json_out($data, int $code=200): never {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$ADMIN = @trim(@file_get_contents(__DIR__.'/.admin_token')) ?: null;
/** Autentica device por token (Authorization: Bearer ... ou X-API-Key) */
function require_token_dev(PDO $pdo): string {
$hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if ($hdr && stripos($hdr,'Bearer ')===0) $hdr = trim(substr($hdr,7));
if ($hdr==='') $hdr = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($hdr==='') json_out(['error'=>'auth required'], 401);
$st = $pdo->prepare("SELECT id FROM device WHERE token=:t");
$st->execute([':t'=>$hdr]);
$id = $st->fetchColumn();
if (!$id) json_out(['error'=>'invalid token'], 401);
return (string)$id;
}
/** Autentica admin por X-Admin-Token */
function require_admin_token(?string $admin): void {
if (!$admin) json_out(['error'=>'admin token missing'], 500);
$hdr = $_SERVER['HTTP_X_ADMIN_TOKEN'] ?? '';
if ($hdr !== $admin) json_out(['error'=>'admin auth'], 401);
}
/* ---------- HEALTH ---------- */
if ($path === '/api/health') json_out(['ok'=>true, 'php'=>PHP_VERSION]);
/* ---------- INGEST ---------- */
if ($path === '/api/ingest' && $method === 'POST') {
$dev = require_token_dev($pdo);
$raw = file_get_contents('php://input') ?: '';
$j = json_decode($raw, true);
if (!is_array($j)) json_out(['error'=>'invalid json'], 400);
$ts = isset($j['ts']) && is_numeric($j['ts']) ? (int)$j['ts'] : time();
$payload = $j; unset($payload['ts']);
// retry leve para "database is locked"
$tries = 0;
while (true) {
try {
$st = $pdo->prepare("INSERT INTO reading(device_id,ts,data) VALUES(:d,:t,:j)");
$st->execute([':d'=>$dev, ':t'=>$ts, ':j'=>json_encode($payload, JSON_UNESCAPED_UNICODE)]);
json_out(['stored'=>true, 'id'=>$pdo->lastInsertId()]);
} catch (PDOException $e) {
if (stripos($e->getMessage(), 'locked') !== false && ++$tries <= 5) { usleep(200000); continue; }
throw $e;
}
}
}
/* ---------- LAST ---------- */
if ($path === '/api/last' && $method === 'GET') {
$dev = $_GET['device'] ?? null; if (!$dev) json_out(['error'=>'device required'], 400);
$st=$pdo->prepare("SELECT ts,data FROM reading WHERE device_id=:d ORDER BY ts DESC LIMIT 1");
$st->execute([':d'=>$dev]); $r=$st->fetch();
if (!$r) json_out(['device'=>$dev,'reading'=>null]);
$r['data'] = json_decode($r['data'], true);
json_out(['device'=>$dev,'reading'=>$r]);
}
/* ---------- DATA ---------- */
if ($path === '/api/data' && $method === 'GET') {
$dev = $_GET['device'] ?? null;
$from= isset($_GET['from']) ? (int)$_GET['from'] : time()-86400;
$to = isset($_GET['to']) ? (int)$_GET['to'] : time();
if (!$dev) json_out(['error'=>'device required'], 400);
$st=$pdo->prepare("SELECT ts,data FROM reading WHERE device_id=:d AND ts BETWEEN :f AND :t ORDER BY ts ASC");
$st->execute([':d'=>$dev, ':f'=>$from, ':t'=>$to]);
$rows=$st->fetchAll();
foreach ($rows as &$r) $r['data']=json_decode($r['data'], true);
json_out(['device'=>$dev,'from'=>$from,'to'=>$to,'rows'=>$rows]);
}
/* ---------- STATE: SET ---------- */
if ($path === '/api/state/set' && $method === 'POST') {
require_admin_token($ADMIN);
$raw=file_get_contents('php://input') ?: '';
$j = json_decode($raw, true);
if (!is_array($j) || empty($j['device'])) json_out(['error'=>'device required'], 400);
$dev = $j['device']; $now = time();
$pairs=[];
if (isset($j['kv']) && is_array($j['kv'])) {
foreach ($j['kv'] as $k=>$v) $pairs[]=[(string)$k, json_encode($v, JSON_UNESCAPED_UNICODE)];
} elseif (isset($j['key'])) {
$pairs[]=[(string)$j['key'], json_encode($j['value'] ?? null, JSON_UNESCAPED_UNICODE)];
} else json_out(['error'=>'payload'], 400);
$st=$pdo->prepare(
"INSERT INTO state(device_id,key,value,updated_ts)
VALUES(:d,:k,:v,:t)
ON CONFLICT(device_id,key)
DO UPDATE SET value=excluded.value, updated_ts=excluded.updated_ts"
);
foreach ($pairs as $p) $st->execute([':d'=>$dev, ':k'=>$p[0], ':v'=>$p[1], ':t'=>$now]);
json_out(['ok'=>true,'updated'=>count($pairs)]);
}
/* ---------- STATE: GET ---------- */
if ($path === '/api/state/get' && $method === 'GET') {
$dev = $_GET['device'] ?? null; if (!$dev) json_out(['error'=>'device required'], 400);
// Admin OU device com token
if (!($ADMIN && (($_SERVER['HTTP_X_ADMIN_TOKEN'] ?? '') === $ADMIN))) {
require_token_dev($pdo);
}
$keys = isset($_GET['keys']) && $_GET['keys']!=='' ? explode(',', $_GET['keys']) : null;
if ($keys) {
$in = rtrim(str_repeat('?,', count($keys)), ',');
$st = $pdo->prepare("SELECT key,value,updated_ts FROM state WHERE device_id=? AND key IN ($in)");
$st->execute(array_merge([$dev], $keys));
} else {
$st = $pdo->prepare("SELECT key,value,updated_ts FROM state WHERE device_id=?");
$st->execute([$dev]);
}
$out=[];
while ($r=$st->fetch()) $out[$r['key']] = json_decode($r['value'], true);
json_out(['device'=>$dev,'kv'=>$out]);
}
/* ---------- COMMANDS: PUSH (admin) ---------- */
if ($path === '/api/commands/push' && $method === 'POST') {
require_admin_token($ADMIN);
$raw=file_get_contents('php://input') ?: '';
$j = json_decode($raw, true);
if (!is_array($j) || empty($j['device']) || empty($j['cmd'])) json_out(['error'=>'device/cmd required'], 400);
$st=$pdo->prepare("INSERT INTO command(device_id,ts,cmd,params,status) VALUES(:d,:t,:c,:p,:s)");
$st->execute([
':d'=>$j['device'], ':t'=>time(), ':c'=>$j['cmd'],
':p'=>isset($j['params']) ? json_encode($j['params'], JSON_UNESCAPED_UNICODE) : null,
':s'=>'queued'
]);
json_out(['queued'=>true,'id'=>$pdo->lastInsertId()]);
}
/* ---------- COMMANDS: POLL (device) ---------- */
if ($path === '/api/commands/poll' && $method === 'GET') {
$dev = require_token_dev($pdo);
$st=$pdo->prepare("SELECT id,cmd,params,ts FROM command WHERE device_id=:d AND status='queued' ORDER BY id ASC LIMIT 10");
$st->execute([':d'=>$dev]);
$rows=$st->fetchAll();
foreach ($rows as &$r) $r['params'] = $r['params'] ? json_decode($r['params'], true) : (object)[];
json_out(['device'=>$dev,'commands'=>$rows]);
}
/* ---------- COMMANDS: ACK (robusto) ---------- */
if ($path === '/api/commands/ack' && $method === 'POST') {
// valida token se quiseres (mas não uses device_id porque a tabela não tem)
require_token_dev($pdo);
$raw = file_get_contents('php://input') ?: '';
$j = json_decode($raw, true) ?: [];
if (empty($j['id'])) json_out(['error' => 'id required'], 400);
// interpreta "success"/"ok"/"status" de forma robusta
$val = $j['success'] ?? $j['ok'] ?? $j['status'] ?? true;
if (is_bool($val)) {
$ok = $val;
} elseif (is_numeric($val)) {
$ok = ((int)$val) === 1;
} elseif (is_string($val)) {
$ok = in_array(strtolower(trim($val)), ['1','true','ok','done','yes','y'], true);
} else {
$ok = true; // default otimista
}
// se tiveres coluna ack_ts, ativa a linha correspondente
$sql = "UPDATE commands
SET status = :status
WHERE id = :i";
$args = [':status' => ($ok ? 'done' : 'failed'), ':i' => (int)$j['id']];
// descomenta se existir a coluna ack_ts
// $sql = "UPDATE commands SET status=:status, ack_ts=:a WHERE id=:i";
// $args[':a'] = time();
$st = $pdo->prepare($sql);
$st->execute($args);
json_out(['updated' => $st->rowCount(), 'id' => (int)$j['id'], 'success' => $ok]);
}
/* ---------- 404 ---------- */
json_out(['error'=>'not found','path'=>$path], 404);