From 92c630bf5879c2ceae0a65d0fecfcee7ce872b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A0=EC=B0=BD=EC=9A=B1?= Date: Sun, 12 Jul 2026 01:02:37 +0900 Subject: [PATCH] feat(cloud): polygon validation + zone config controller (RBAC) --- cloud/src/Dashboard/ZoneConfigController.php | 59 ++++++++++++++++++++ cloud/src/Geometry/Polygon.php | 16 ++++++ cloud/tests/ZoneConfigTest.php | 41 ++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 cloud/src/Dashboard/ZoneConfigController.php create mode 100644 cloud/src/Geometry/Polygon.php create mode 100644 cloud/tests/ZoneConfigTest.php diff --git a/cloud/src/Dashboard/ZoneConfigController.php b/cloud/src/Dashboard/ZoneConfigController.php new file mode 100644 index 0000000..12fec0b --- /dev/null +++ b/cloud/src/Dashboard/ZoneConfigController.php @@ -0,0 +1,59 @@ +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); + } +} diff --git a/cloud/src/Geometry/Polygon.php b/cloud/src/Geometry/Polygon.php new file mode 100644 index 0000000..2e5a856 --- /dev/null +++ b/cloud/src/Geometry/Polygon.php @@ -0,0 +1,16 @@ +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()); + } +}