feat(cloud): dashboard list/detail/ack + auth-gated snapshot proxy
This commit is contained in:
parent
430758c3b7
commit
90d3016b17
4 changed files with 123 additions and 0 deletions
35
cloud/src/Dashboard/EventDetailController.php
Normal file
35
cloud/src/Dashboard/EventDetailController.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace App\Dashboard;
|
||||
|
||||
use App\Http\{Request, Response};
|
||||
use App\Auth\{SessionStore, Rbac};
|
||||
use App\Repository\{EventRepository, SnapshotRepository, AuditRepository};
|
||||
|
||||
final class EventDetailController
|
||||
{
|
||||
public function __construct(
|
||||
private EventRepository $events, private SnapshotRepository $snaps,
|
||||
private AuditRepository $audit, private SessionStore $sess
|
||||
) {}
|
||||
|
||||
public function show(Request $req, array $params): Response
|
||||
{
|
||||
if ($this->sess->get('user_id') === null) return Response::json(['error' => 'unauthorized'], 401);
|
||||
$id = (int)($params['id'] ?? 0);
|
||||
$event = $this->events->find($id);
|
||||
if ($event === null) return Response::json(['error' => 'not_found'], 404);
|
||||
return Response::json(['event' => $event, 'snapshots' => $this->snaps->forEvent($id)], 200);
|
||||
}
|
||||
|
||||
public function acknowledge(Request $req, array $params): Response
|
||||
{
|
||||
if ($this->sess->get('user_id') === null) return Response::json(['error' => 'unauthorized'], 401);
|
||||
if (!Rbac::can((string)$this->sess->get('role'), 'acknowledge')) return Response::json(['error' => 'forbidden'], 403);
|
||||
$id = (int)($params['id'] ?? 0);
|
||||
$user = (string)$this->sess->get('username');
|
||||
if (!$this->events->acknowledge($id, $user)) return Response::json(['error' => 'not_found'], 404);
|
||||
$this->audit->append($user, 'acknowledge', "event:$id", [], []);
|
||||
return Response::json(['ok' => true], 200);
|
||||
}
|
||||
}
|
||||
20
cloud/src/Dashboard/EventListController.php
Normal file
20
cloud/src/Dashboard/EventListController.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace App\Dashboard;
|
||||
|
||||
use App\Http\{Request, Response};
|
||||
use App\Auth\SessionStore;
|
||||
use App\Repository\EventRepository;
|
||||
|
||||
final class EventListController
|
||||
{
|
||||
public function __construct(private EventRepository $events, private SessionStore $sess) {}
|
||||
|
||||
public function json(Request $req): Response
|
||||
{
|
||||
if ($this->sess->get('user_id') === null) return Response::json(['error' => 'unauthorized'], 401);
|
||||
$severity = $req->query('severity');
|
||||
$rows = $this->events->recent(100, $severity);
|
||||
return Response::json(['events' => $rows], 200);
|
||||
}
|
||||
}
|
||||
21
cloud/src/Dashboard/SnapshotProxy.php
Normal file
21
cloud/src/Dashboard/SnapshotProxy.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace App\Dashboard;
|
||||
|
||||
use App\Http\{Request, Response};
|
||||
use App\Auth\SessionStore;
|
||||
use App\Repository\SnapshotRepository;
|
||||
|
||||
final class SnapshotProxy
|
||||
{
|
||||
public function __construct(private SnapshotRepository $snaps, private SessionStore $sess) {}
|
||||
|
||||
public function serve(Request $req, array $params): Response
|
||||
{
|
||||
if ($this->sess->get('user_id') === null) return Response::json(['error' => 'unauthorized'], 401);
|
||||
$uuid = (string)($params['uuid'] ?? '');
|
||||
$path = $this->snaps->pathByUuid($uuid);
|
||||
if ($path === null || !is_file($path)) return Response::json(['error' => 'not_found'], 404);
|
||||
return Response::bytes((string)file_get_contents($path), 'image/jpeg', 200);
|
||||
}
|
||||
}
|
||||
47
cloud/tests/DashboardTest.php
Normal file
47
cloud/tests/DashboardTest.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace App\Tests;
|
||||
use App\Http\Request;
|
||||
use App\Auth\ArraySessionStore;
|
||||
use App\Dashboard\{EventListController, EventDetailController, SnapshotProxy};
|
||||
use App\Tests\Support\{InMemoryEventRepository, InMemorySnapshotRepository, InMemoryAuditRepository};
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class DashboardTest 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 testListRequiresAuth(): void {
|
||||
$repo = new InMemoryEventRepository();
|
||||
$c = new EventListController($repo, new ArraySessionStore());
|
||||
$this->assertSame(401, $c->json(new Request('GET', '/api/events', [], '', [], []))->status());
|
||||
}
|
||||
public function testListReturnsRecent(): void {
|
||||
$repo = new InMemoryEventRepository();
|
||||
$repo->insert(['severity' => 'high', 'event_type' => 'fall']);
|
||||
$c = new EventListController($repo, $this->authed());
|
||||
$res = $c->json(new Request('GET', '/api/events', [], '', [], []));
|
||||
$this->assertSame(200, $res->status());
|
||||
$this->assertCount(1, json_decode($res->body(), true)['events']);
|
||||
}
|
||||
public function testAcknowledgeRequiresPermissionAndSetsUser(): void {
|
||||
$repo = new InMemoryEventRepository();
|
||||
$id = $repo->insert(['severity' => 'critical', 'event_type' => 'no_motion']);
|
||||
$req = new Request('POST', "/events/$id/ack", [], '', [], []);
|
||||
// viewer denied
|
||||
$cv = new EventDetailController($repo, new InMemorySnapshotRepository(), new InMemoryAuditRepository(), $this->authed('viewer'));
|
||||
$this->assertSame(403, $cv->acknowledge($req, ['id' => (string)$id])->status());
|
||||
// operator allowed
|
||||
$co = new EventDetailController($repo, new InMemorySnapshotRepository(), new InMemoryAuditRepository(), $this->authed('operator'));
|
||||
$this->assertSame(200, $co->acknowledge($req, ['id' => (string)$id])->status());
|
||||
$this->assertSame('u', $repo->find($id)['acknowledged_by']);
|
||||
}
|
||||
public function testSnapshotProxyAuthAnd404(): void {
|
||||
$snaps = new InMemorySnapshotRepository();
|
||||
$proxy = new SnapshotProxy($snaps, new ArraySessionStore()); // unauth
|
||||
$this->assertSame(401, $proxy->serve(new Request('GET', '/snap/x', [], '', [], []), ['uuid' => 'x'])->status());
|
||||
$proxy2 = new SnapshotProxy($snaps, $this->authed());
|
||||
$this->assertSame(404, $proxy2->serve(new Request('GET', '/snap/missing', [], '', [], []), ['uuid' => 'missing'])->status());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue