feat(cloud): polygon validation + zone config controller (RBAC)
This commit is contained in:
parent
90d3016b17
commit
92c630bf58
3 changed files with 116 additions and 0 deletions
59
cloud/src/Dashboard/ZoneConfigController.php
Normal file
59
cloud/src/Dashboard/ZoneConfigController.php
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace App\Dashboard;
|
||||||
|
|
||||||
|
use App\Http\{Request, Response};
|
||||||
|
use App\Auth\{SessionStore, Rbac};
|
||||||
|
use App\Geometry\Polygon;
|
||||||
|
use App\Repository\{ZoneRepository, AuditRepository};
|
||||||
|
|
||||||
|
final class ZoneConfigController
|
||||||
|
{
|
||||||
|
private const LEVELS = ['warning','restricted','pinch'];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private ZoneRepository $zones, private AuditRepository $audit, private SessionStore $sess
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private function guard(string $perm): ?Response
|
||||||
|
{
|
||||||
|
if ($this->sess->get('user_id') === null) return Response::json(['error' => 'unauthorized'], 401);
|
||||||
|
if (!Rbac::can((string)$this->sess->get('role'), $perm)) return Response::json(['error' => 'forbidden'], 403);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function list(Request $req, array $params): Response
|
||||||
|
{
|
||||||
|
if ($this->sess->get('user_id') === null) return Response::json(['error' => 'unauthorized'], 401);
|
||||||
|
return Response::json(['zones' => $this->zones->forCamera((string)($params['camera'] ?? ''))], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Request $req): Response
|
||||||
|
{
|
||||||
|
if ($g = $this->guard('edit_zones')) return $g;
|
||||||
|
$b = $req->json();
|
||||||
|
$level = (string)($b['level'] ?? '');
|
||||||
|
$polygon = $b['polygon'] ?? [];
|
||||||
|
if (!in_array($level, self::LEVELS, true) || !is_array($polygon) || !Polygon::validate($polygon)) {
|
||||||
|
return Response::json(['error' => 'invalid_zone'], 400);
|
||||||
|
}
|
||||||
|
$zoneId = $this->zones->save([
|
||||||
|
'zone_id' => $b['zone_id'] ?? null,
|
||||||
|
'device_id' => (string)($b['device_id'] ?? ''),
|
||||||
|
'camera_id' => (string)($b['camera_id'] ?? ''),
|
||||||
|
'level' => $level,
|
||||||
|
'polygon_json' => json_encode($polygon),
|
||||||
|
]);
|
||||||
|
$this->audit->append((string)$this->sess->get('username'), 'zone_save', "zone:$zoneId", [], ['level' => $level]);
|
||||||
|
return Response::json(['zone_id' => $zoneId], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Request $req, array $params): Response
|
||||||
|
{
|
||||||
|
if ($g = $this->guard('edit_zones')) return $g;
|
||||||
|
$id = (string)($params['id'] ?? '');
|
||||||
|
if (!$this->zones->delete($id)) return Response::json(['error' => 'not_found'], 404);
|
||||||
|
$this->audit->append((string)$this->sess->get('username'), 'zone_delete', "zone:$id", [], []);
|
||||||
|
return Response::json(['ok' => true], 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
cloud/src/Geometry/Polygon.php
Normal file
16
cloud/src/Geometry/Polygon.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace App\Geometry;
|
||||||
|
|
||||||
|
final class Polygon
|
||||||
|
{
|
||||||
|
public static function validate(array $points): bool
|
||||||
|
{
|
||||||
|
if (count($points) < 3) return false;
|
||||||
|
foreach ($points as $pt) {
|
||||||
|
if (!is_array($pt) || count($pt) !== 2) return false;
|
||||||
|
if (!is_numeric($pt[0]) || !is_numeric($pt[1])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
cloud/tests/ZoneConfigTest.php
Normal file
41
cloud/tests/ZoneConfigTest.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
namespace App\Tests;
|
||||||
|
use App\Http\Request;
|
||||||
|
use App\Auth\ArraySessionStore;
|
||||||
|
use App\Geometry\Polygon;
|
||||||
|
use App\Dashboard\ZoneConfigController;
|
||||||
|
use App\Tests\Support\{InMemoryZoneRepository, InMemoryAuditRepository};
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class ZoneConfigTest extends TestCase
|
||||||
|
{
|
||||||
|
private function authed(string $role = 'operator'): ArraySessionStore {
|
||||||
|
$s = new ArraySessionStore(); $s->set('user_id', 1); $s->set('username', 'u'); $s->set('role', $role); return $s;
|
||||||
|
}
|
||||||
|
public function testPolygonValidation(): void {
|
||||||
|
$this->assertTrue(Polygon::validate([[0,0],[10,0],[10,10]]));
|
||||||
|
$this->assertFalse(Polygon::validate([[0,0],[10,0]])); // <3
|
||||||
|
$this->assertFalse(Polygon::validate([[0,0],[10,0],['x',10]])); // non-numeric
|
||||||
|
}
|
||||||
|
private function saveReq(array $body): Request {
|
||||||
|
return new Request('POST', '/api/zones', [], json_encode($body), [], []);
|
||||||
|
}
|
||||||
|
public function testSaveRequiresPermission(): void {
|
||||||
|
$c = new ZoneConfigController(new InMemoryZoneRepository(), new InMemoryAuditRepository(), $this->authed('viewer'));
|
||||||
|
$res = $c->save($this->saveReq(['camera_id' => 'cam-1', 'level' => 'restricted', 'polygon' => [[0,0],[9,0],[9,9]]]));
|
||||||
|
$this->assertSame(403, $res->status());
|
||||||
|
}
|
||||||
|
public function testSaveValidZone(): void {
|
||||||
|
$repo = new InMemoryZoneRepository();
|
||||||
|
$c = new ZoneConfigController($repo, new InMemoryAuditRepository(), $this->authed('operator'));
|
||||||
|
$res = $c->save($this->saveReq(['device_id' => 'pi-1', 'camera_id' => 'cam-1', 'level' => 'restricted', 'polygon' => [[0,0],[9,0],[9,9]]]));
|
||||||
|
$this->assertSame(201, $res->status());
|
||||||
|
$this->assertCount(1, $repo->forCamera('cam-1'));
|
||||||
|
}
|
||||||
|
public function testSaveRejectsBadLevelOrPolygon(): void {
|
||||||
|
$c = new ZoneConfigController(new InMemoryZoneRepository(), new InMemoryAuditRepository(), $this->authed('operator'));
|
||||||
|
$this->assertSame(400, $c->save($this->saveReq(['camera_id' => 'cam-1', 'level' => 'BOOM', 'polygon' => [[0,0],[9,0],[9,9]]]))->status());
|
||||||
|
$this->assertSame(400, $c->save($this->saveReq(['camera_id' => 'cam-1', 'level' => 'warning', 'polygon' => [[0,0]]]))->status());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue