refactor(cloud): prefix all DB tables with move_
Renames the 10 P1-B tables (users, devices, cameras, zones, events, event_snapshots, thresholds, nonces, rate_limits, audit_log) to a move_ prefix so the schema can co-tenant a shared-hosting database. - schema.sql + all App\Repository\Pdo\* queries + tests + deploy docs - new migrations/2026-07-20-move-table-prefix.sql: atomic RENAME TABLE for existing installs; must run BEFORE deploying this code - index/constraint names left unchanged (no cross-table collision) Tests: 109 pass, 4 skipped.
This commit is contained in:
parent
3a8a5106cd
commit
5d511b04f2
15 changed files with 106 additions and 82 deletions
|
|
@ -129,7 +129,7 @@ php -r "echo password_hash('여기에_진짜_비밀번호', PASSWORD_ARGON2ID),
|
|||
phpMyAdmin → SQL 탭:
|
||||
|
||||
```sql
|
||||
INSERT INTO users (username, password_hash, role, site_scope, failed_attempts, locked_until)
|
||||
INSERT INTO move_users (username, password_hash, role, site_scope, failed_attempts, locked_until)
|
||||
VALUES ('admin', '여기에_해시_붙여넣기', 'admin', NULL, 0, 0);
|
||||
```
|
||||
|
||||
|
|
@ -144,7 +144,7 @@ php -r "echo bin2hex(random_bytes(32)), \"\n\";"
|
|||
```
|
||||
|
||||
```sql
|
||||
INSERT INTO devices (device_id, name, secret, config_version)
|
||||
INSERT INTO move_devices (device_id, name, secret, config_version)
|
||||
VALUES ('pi-01', '3라인 낙상 카메라', '여기에_비밀키_붙여넣기', 1);
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -188,14 +188,14 @@ php -r "echo bin2hex(random_bytes(32)), \"\n\";"
|
|||
```
|
||||
|
||||
```sql
|
||||
INSERT INTO devices (device_id, name, secret, config_version)
|
||||
INSERT INTO move_devices (device_id, name, secret, config_version)
|
||||
VALUES ('pi-01', 'Line 3 — Fall Cam', '<paste the generated secret here>', 1);
|
||||
```
|
||||
|
||||
Deliver the secret to the device provisioning process out-of-band (not email/chat) and
|
||||
configure it as the device's `HMAC` signing key for `X-Signature` on
|
||||
`/api/events`, `/api/heartbeat`, and `/api/config`. Rotate a compromised secret by
|
||||
`UPDATE devices SET secret = ? WHERE device_id = ?` with a freshly generated value — the
|
||||
`UPDATE move_devices SET secret = ? WHERE device_id = ?` with a freshly generated value — the
|
||||
old value stops working immediately since `secretFor()` always reads the current row.
|
||||
|
||||
## 7. First admin account
|
||||
|
|
@ -215,7 +215,7 @@ php -r "echo password_hash('CHANGE-ME-to-a-real-passphrase', PASSWORD_ARGON2ID),
|
|||
`PASSWORD_ARGON2ID`.)
|
||||
|
||||
```sql
|
||||
INSERT INTO users (username, password_hash, role, site_scope, failed_attempts, locked_until)
|
||||
INSERT INTO move_users (username, password_hash, role, site_scope, failed_attempts, locked_until)
|
||||
VALUES ('admin', '<paste the argon2id hash here>', 'admin', NULL, 0, 0);
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -21,27 +21,27 @@
|
|||
-- ===========================================================================
|
||||
|
||||
-- A1) devices.site — the access-control site each device belongs to.
|
||||
ALTER TABLE devices
|
||||
ALTER TABLE move_devices
|
||||
ADD COLUMN site VARCHAR(64) NULL AFTER name,
|
||||
ADD KEY ix_devices_site (site);
|
||||
|
||||
-- A2) events.site — denormalized from devices.site at ingest, for the dashboard's
|
||||
-- A2) events.site — denormalized from move_devices.site at ingest, for the dashboard's
|
||||
-- site-scoped event list.
|
||||
ALTER TABLE events
|
||||
ALTER TABLE move_events
|
||||
ADD COLUMN site VARCHAR(64) NULL AFTER device_id,
|
||||
ADD KEY ix_events_site_occurred_at (site, occurred_at);
|
||||
|
||||
-- A3) events.event_uuid — edge-generated idempotency key (P1-9). The UNIQUE key
|
||||
-- makes a re-sent event collide (dedup) instead of duplicating. MySQL allows
|
||||
-- multiple NULLs in a UNIQUE index, so legacy rows without a uuid are fine.
|
||||
ALTER TABLE events
|
||||
ALTER TABLE move_events
|
||||
ADD COLUMN event_uuid CHAR(32) NULL AFTER event_id,
|
||||
ADD UNIQUE KEY uq_events_uuid (event_uuid);
|
||||
|
||||
-- A4) event_snapshots.created_at index — PdoSnapshotRepository::purgeOlderThan()
|
||||
-- runs "SELECT ... WHERE created_at < ?" inside live ingest requests. Without
|
||||
-- this index it full-scans the snapshot table on every triggered purge.
|
||||
ALTER TABLE event_snapshots
|
||||
ALTER TABLE move_event_snapshots
|
||||
ADD KEY ix_event_snapshots_created (created_at);
|
||||
|
||||
-- A5) event_snapshots (event_id, content_hash) UNIQUE — lets the ingest dedup path
|
||||
|
|
@ -50,21 +50,21 @@ ALTER TABLE event_snapshots
|
|||
-- collides instead of writing a second copy). If this fails on "Duplicate
|
||||
-- entry", the table already has duplicate (event_id, content_hash) rows —
|
||||
-- remove the dups, then re-run this one statement.
|
||||
ALTER TABLE event_snapshots
|
||||
ALTER TABLE move_event_snapshots
|
||||
ADD UNIQUE KEY uq_event_snapshots_event_hash (event_id, content_hash);
|
||||
|
||||
-- A6) rate_limits.window_start index — the reap "DELETE ... WHERE window_start < ?"
|
||||
-- cannot use the (rate_key, window_start) PRIMARY KEY (window_start is the
|
||||
-- trailing column), so it full-scans and locks rows on the pre-auth hot path.
|
||||
ALTER TABLE rate_limits
|
||||
ALTER TABLE move_rate_limits
|
||||
ADD KEY ix_rate_limits_window (window_start);
|
||||
|
||||
-- ===========================================================================
|
||||
-- PART B — provisioning (EDIT the values for your deployment, then run).
|
||||
-- Assign every device to its site here; the backfill in Part C reads it.
|
||||
-- ===========================================================================
|
||||
-- UPDATE devices SET site = 'factory-a' WHERE device_id = 'pi-1';
|
||||
-- UPDATE devices SET site = 'factory-b' WHERE device_id = 'pi-2';
|
||||
-- UPDATE move_devices SET site = 'factory-a' WHERE device_id = 'pi-1';
|
||||
-- UPDATE move_devices SET site = 'factory-b' WHERE device_id = 'pi-2';
|
||||
-- -- ... one row per device ...
|
||||
|
||||
-- ===========================================================================
|
||||
|
|
@ -72,8 +72,8 @@ ALTER TABLE rate_limits
|
|||
-- RUN THIS ONLY AFTER Part B. Re-run it whenever you add/relabel a
|
||||
-- device's site so its past events reappear for scoped operators.
|
||||
-- ===========================================================================
|
||||
UPDATE events e
|
||||
JOIN devices d ON d.device_id = e.device_id
|
||||
UPDATE move_events e
|
||||
JOIN move_devices d ON d.device_id = e.device_id
|
||||
SET e.site = d.site
|
||||
WHERE e.site IS NULL AND d.site IS NOT NULL;
|
||||
|
||||
|
|
@ -82,17 +82,17 @@ UPDATE events e
|
|||
-- see their backfilled history). NULL = global operator; '' is NOT
|
||||
-- global (matches nothing — an accidental blank fails closed).
|
||||
-- ===========================================================================
|
||||
-- UPDATE users SET site_scope = 'factory-a' WHERE username = 'operator-a';
|
||||
-- UPDATE move_users SET site_scope = 'factory-a' WHERE username = 'operator-a';
|
||||
|
||||
-- ---- Verification (run these; each must confirm the object/state) -----------
|
||||
-- SHOW COLUMNS FROM devices LIKE 'site';
|
||||
-- SHOW COLUMNS FROM events LIKE 'site';
|
||||
-- SHOW COLUMNS FROM events LIKE 'event_uuid';
|
||||
-- SHOW INDEX FROM events WHERE Key_name = 'uq_events_uuid';
|
||||
-- SHOW INDEX FROM event_snapshots WHERE Key_name = 'ix_event_snapshots_created';
|
||||
-- SHOW INDEX FROM event_snapshots WHERE Key_name = 'uq_event_snapshots_event_hash';
|
||||
-- SHOW INDEX FROM rate_limits WHERE Key_name = 'ix_rate_limits_window';
|
||||
-- SHOW COLUMNS FROM move_devices LIKE 'site';
|
||||
-- SHOW COLUMNS FROM move_events LIKE 'site';
|
||||
-- SHOW COLUMNS FROM move_events LIKE 'event_uuid';
|
||||
-- SHOW INDEX FROM move_events WHERE Key_name = 'uq_events_uuid';
|
||||
-- SHOW INDEX FROM move_event_snapshots WHERE Key_name = 'ix_event_snapshots_created';
|
||||
-- SHOW INDEX FROM move_event_snapshots WHERE Key_name = 'uq_event_snapshots_event_hash';
|
||||
-- SHOW INDEX FROM move_rate_limits WHERE Key_name = 'ix_rate_limits_window';
|
||||
-- Events still unscoped after Part C (expect 0, or only rows whose device has no
|
||||
-- site assigned yet — go back and finish Part B for those devices):
|
||||
-- SELECT COUNT(*) FROM events e JOIN devices d ON d.device_id = e.device_id
|
||||
-- SELECT COUNT(*) FROM move_events e JOIN move_devices d ON d.device_id = e.device_id
|
||||
-- WHERE e.site IS NULL AND d.site IS NOT NULL;
|
||||
|
|
|
|||
19
cloud/migrations/2026-07-20-move-table-prefix.sql
Normal file
19
cloud/migrations/2026-07-20-move-table-prefix.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- cloud/migrations/2026-07-20-move-table-prefix.sql
|
||||
-- Rename every P1-B table to the `move_` prefix (shared-hosting DB co-tenancy).
|
||||
-- Run ONCE in phpMyAdmin BEFORE deploying code that reads the prefixed names.
|
||||
-- Fresh installs: skip this, cloud/schema.sql already creates prefixed tables.
|
||||
--
|
||||
-- RENAME TABLE is atomic across all pairs and preserves indexes, constraints
|
||||
-- and FKs (fk_event_snapshots_event follows its parent automatically).
|
||||
|
||||
RENAME TABLE
|
||||
users TO move_users,
|
||||
devices TO move_devices,
|
||||
cameras TO move_cameras,
|
||||
zones TO move_zones,
|
||||
events TO move_events,
|
||||
event_snapshots TO move_event_snapshots,
|
||||
thresholds TO move_thresholds,
|
||||
nonces TO move_nonces,
|
||||
rate_limits TO move_rate_limits,
|
||||
audit_log TO move_audit_log;
|
||||
|
|
@ -36,7 +36,7 @@ SET NAMES utf8mb4;
|
|||
-- No PdoUserRepository::create() exists in P1-B -- the first admin account is
|
||||
-- provisioned out-of-band via the CLI snippet in cloud/docs/cloud-deploy.md.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE users (
|
||||
CREATE TABLE move_users (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
username VARCHAR(190) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL, -- Argon2id, see App\Security\Password::hash()
|
||||
|
|
@ -55,7 +55,7 @@ CREATE TABLE users (
|
|||
-- No PdoDeviceRepository::create() exists -- rows are provisioned by an admin
|
||||
-- (per-device secret issuance) before the device is allowed to sync/ingest.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE devices (
|
||||
CREATE TABLE move_devices (
|
||||
device_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(190) NULL,
|
||||
site VARCHAR(64) NULL, -- access-control scope; matches users.site_scope (NULL = unassigned)
|
||||
|
|
@ -77,7 +77,7 @@ CREATE TABLE devices (
|
|||
-- directly as a free-form string. Kept for documentation/future per-camera
|
||||
-- config and to avoid a breaking migration when that repository is added.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE cameras (
|
||||
CREATE TABLE move_cameras (
|
||||
camera_id VARCHAR(64) NOT NULL,
|
||||
device_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(190) NULL,
|
||||
|
|
@ -94,7 +94,7 @@ CREATE TABLE cameras (
|
|||
-- `cameras` row exists for a given camera_id (no camera-registration flow in
|
||||
-- P1-B), so a FK here would spuriously reject valid app writes.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE zones (
|
||||
CREATE TABLE move_zones (
|
||||
zone_id CHAR(32) NOT NULL, -- bin2hex(random_bytes(16)), same convention as snapshot uuids
|
||||
device_id VARCHAR(64) NOT NULL,
|
||||
camera_id VARCHAR(64) NOT NULL,
|
||||
|
|
@ -112,11 +112,11 @@ CREATE TABLE zones (
|
|||
-- App\Ingest\EventValidator before insert(); the CHECK constraints below are
|
||||
-- defense-in-depth only, not the primary validation layer.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE events (
|
||||
CREATE TABLE move_events (
|
||||
event_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
event_uuid CHAR(32) NULL, -- edge-generated idempotency key (dedup a re-sent event); NULL for legacy rows
|
||||
device_id VARCHAR(64) NOT NULL,
|
||||
site VARCHAR(64) NULL, -- denormalized from devices.site at ingest; access-control scope
|
||||
site VARCHAR(64) NULL, -- denormalized from move_devices.site at ingest; access-control scope
|
||||
camera_id VARCHAR(64) NOT NULL,
|
||||
event_type VARCHAR(32) NOT NULL,
|
||||
severity VARCHAR(16) NOT NULL,
|
||||
|
|
@ -150,7 +150,7 @@ CREATE TABLE events (
|
|||
-- events is safe because IngestController always inserts the parent event row
|
||||
-- before calling SnapshotRepository::add() for its snapshots.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE event_snapshots (
|
||||
CREATE TABLE move_event_snapshots (
|
||||
snapshot_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
event_id INT UNSIGNED NOT NULL,
|
||||
file_uuid CHAR(32) NOT NULL, -- bin2hex(random_bytes(16)); on-disk filename = <uuid>.jpg
|
||||
|
|
@ -164,7 +164,7 @@ CREATE TABLE event_snapshots (
|
|||
UNIQUE KEY uq_event_snapshots_event_hash (event_id, content_hash),
|
||||
KEY ix_event_snapshots_event (event_id),
|
||||
KEY ix_event_snapshots_created (created_at), -- retention purge scans WHERE created_at < ?
|
||||
CONSTRAINT fk_event_snapshots_event FOREIGN KEY (event_id) REFERENCES events (event_id)
|
||||
CONSTRAINT fk_event_snapshots_event FOREIGN KEY (event_id) REFERENCES move_events (event_id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ CREATE TABLE event_snapshots (
|
|||
-- GET /api/config. No PdoThresholdRepository write method exists in P1-B --
|
||||
-- rows are seeded/edited directly (phpMyAdmin) until a management UI lands.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE thresholds (
|
||||
CREATE TABLE move_thresholds (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
device_id VARCHAR(64) NOT NULL,
|
||||
camera_id VARCHAR(64) NULL,
|
||||
|
|
@ -194,11 +194,11 @@ CREATE TABLE thresholds (
|
|||
-- TOCTOU flagged in the Task 7 review -- the PK uniqueness is the real guard,
|
||||
-- not the earlier seen() check).
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE nonces (
|
||||
CREATE TABLE move_nonces (
|
||||
nonce VARCHAR(190) NOT NULL,
|
||||
expires_at BIGINT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (nonce),
|
||||
KEY ix_nonces_expires_at (expires_at) -- for a periodic `DELETE FROM nonces WHERE expires_at < ?` cleanup job
|
||||
KEY ix_nonces_expires_at (expires_at) -- for a periodic `DELETE FROM move_nonces WHERE expires_at < ?` cleanup job
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
|
@ -207,7 +207,7 @@ CREATE TABLE nonces (
|
|||
-- `INSERT ... ON DUPLICATE KEY UPDATE count = count + 1` upsert in
|
||||
-- PdoRateLimiterRepository::hit() -- no read-modify-write race window.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE rate_limits (
|
||||
CREATE TABLE move_rate_limits (
|
||||
rate_key VARCHAR(190) NOT NULL,
|
||||
window_start BIGINT UNSIGNED NOT NULL, -- floor(now / 60): the minute bucket
|
||||
count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
|
|
@ -222,7 +222,7 @@ CREATE TABLE rate_limits (
|
|||
-- Tamper-evident hash-chaining (design doc §9.2) is deferred to P3 per the
|
||||
-- Task 16 brief self-review -- this table is the plain append-only record only.
|
||||
-- ---------------------------------------------------------------------------
|
||||
CREATE TABLE audit_log (
|
||||
CREATE TABLE move_audit_log (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
actor VARCHAR(190) NOT NULL,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
|
|
@ -241,13 +241,18 @@ CREATE TABLE audit_log (
|
|||
-- run. Apply the RUNNABLE migration instead:
|
||||
--
|
||||
-- cloud/migrations/2026-07-17-p1-9-p3-2b.sql
|
||||
-- cloud/migrations/2026-07-20-move-table-prefix.sql (run this one LAST)
|
||||
--
|
||||
-- The second migration renames every table to the `move_` prefix used above and
|
||||
-- by App\Repository\Pdo\* -- an existing DB must be renamed before deploy or all
|
||||
-- queries fail with "table doesn't exist".
|
||||
--
|
||||
-- It adds devices.site / events.site / events.event_uuid, the retention and
|
||||
-- rate-limit reap indexes, AND backfills events.site from devices.site (without
|
||||
-- rate-limit reap indexes, AND backfills events.site from move_devices.site (without
|
||||
-- that backfill, historical events vanish for a scoped operator). Run it once in
|
||||
-- phpMyAdmin BEFORE deploying code that reads these columns, then provision:
|
||||
-- UPDATE devices SET site = 'factory-a' WHERE device_id = 'pi-1';
|
||||
-- UPDATE users SET site_scope = 'factory-a' WHERE username = 'operator-a';
|
||||
-- UPDATE move_devices SET site = 'factory-a' WHERE device_id = 'pi-1';
|
||||
-- UPDATE move_users SET site_scope = 'factory-a' WHERE username = 'operator-a';
|
||||
-- A user with site_scope = NULL is a global operator; '' is NOT global (matches
|
||||
-- nothing — a blank scope fails closed).
|
||||
-- ===========================================================================
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ final class PdoAuditRepository implements AuditRepository
|
|||
public function append(string $actor, string $action, string $target, array $before, array $after): void
|
||||
{
|
||||
$st = $this->pdo->prepare(
|
||||
'INSERT INTO audit_log (actor, action, target, before_json, after_json, ts)
|
||||
'INSERT INTO move_audit_log (actor, action, target, before_json, after_json, ts)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$st->execute([
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ final class PdoDeviceRepository implements DeviceRepository
|
|||
|
||||
public function secretFor(string $deviceId): ?string
|
||||
{
|
||||
$st = $this->pdo->prepare('SELECT secret FROM devices WHERE device_id = ? LIMIT 1');
|
||||
$st = $this->pdo->prepare('SELECT secret FROM move_devices WHERE device_id = ? LIMIT 1');
|
||||
$st->execute([$deviceId]);
|
||||
$secret = $st->fetchColumn();
|
||||
return $secret === false ? null : (string)$secret;
|
||||
|
|
@ -19,7 +19,7 @@ final class PdoDeviceRepository implements DeviceRepository
|
|||
|
||||
public function siteFor(string $deviceId): ?string
|
||||
{
|
||||
$st = $this->pdo->prepare('SELECT site FROM devices WHERE device_id = ? LIMIT 1');
|
||||
$st = $this->pdo->prepare('SELECT site FROM move_devices WHERE device_id = ? LIMIT 1');
|
||||
$st->execute([$deviceId]);
|
||||
$site = $st->fetchColumn();
|
||||
// false = no such device; null = device exists but unassigned. Both -> null (unscoped).
|
||||
|
|
@ -34,7 +34,7 @@ final class PdoDeviceRepository implements DeviceRepository
|
|||
// out-of-band by an admin (see cloud/docs/cloud-deploy.md), never created
|
||||
// implicitly from an inbound heartbeat.
|
||||
$st = $this->pdo->prepare(
|
||||
'UPDATE devices
|
||||
'UPDATE move_devices
|
||||
SET last_fps = ?, last_temp = ?, last_throttle = ?,
|
||||
reported_config_version = ?, last_heartbeat_at = ?
|
||||
WHERE device_id = ?'
|
||||
|
|
@ -51,7 +51,7 @@ final class PdoDeviceRepository implements DeviceRepository
|
|||
|
||||
public function configVersion(string $deviceId): int
|
||||
{
|
||||
$st = $this->pdo->prepare('SELECT config_version FROM devices WHERE device_id = ? LIMIT 1');
|
||||
$st = $this->pdo->prepare('SELECT config_version FROM move_devices WHERE device_id = ? LIMIT 1');
|
||||
$st->execute([$deviceId]);
|
||||
$version = $st->fetchColumn();
|
||||
return $version === false ? 1 : (int)$version;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ final class PdoEventRepository implements EventRepository
|
|||
public function insert(array $event): int
|
||||
{
|
||||
$st = $this->pdo->prepare(
|
||||
'INSERT INTO events
|
||||
'INSERT INTO move_events
|
||||
(event_uuid, device_id, site, camera_id, event_type, severity, track_id, zone_id,
|
||||
occurred_at, duration_s, metrics_json, received_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
|
|
@ -24,7 +24,7 @@ final class PdoEventRepository implements EventRepository
|
|||
$st->execute([
|
||||
$event['event_uuid'] ?? null, // NULL when the edge sent no idempotency key (legacy)
|
||||
(string)$event['device_id'],
|
||||
$event['site'] ?? null, // denormalized from devices.site by IngestController
|
||||
$event['site'] ?? null, // denormalized from move_devices.site by IngestController
|
||||
(string)$event['camera_id'],
|
||||
(string)$event['event_type'],
|
||||
(string)$event['severity'],
|
||||
|
|
@ -40,7 +40,7 @@ final class PdoEventRepository implements EventRepository
|
|||
|
||||
public function idByUuid(string $uuid): ?int
|
||||
{
|
||||
$st = $this->pdo->prepare('SELECT event_id FROM events WHERE event_uuid = ? LIMIT 1');
|
||||
$st = $this->pdo->prepare('SELECT event_id FROM move_events WHERE event_uuid = ? LIMIT 1');
|
||||
$st->execute([$uuid]);
|
||||
$id = $st->fetchColumn();
|
||||
return $id === false ? null : (int)$id;
|
||||
|
|
@ -51,7 +51,7 @@ final class PdoEventRepository implements EventRepository
|
|||
if ($severity !== null) {
|
||||
$st = $this->pdo->prepare(
|
||||
'SELECT ' . self::SELECT_COLUMNS . '
|
||||
FROM events WHERE severity = ?
|
||||
FROM move_events WHERE severity = ?
|
||||
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
|
||||
);
|
||||
$st->bindValue(1, $severity, PDO::PARAM_STR);
|
||||
|
|
@ -59,7 +59,7 @@ final class PdoEventRepository implements EventRepository
|
|||
} else {
|
||||
$st = $this->pdo->prepare(
|
||||
'SELECT ' . self::SELECT_COLUMNS . '
|
||||
FROM events
|
||||
FROM move_events
|
||||
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
|
||||
);
|
||||
$st->bindValue(1, $limit, PDO::PARAM_INT);
|
||||
|
|
@ -92,7 +92,7 @@ final class PdoEventRepository implements EventRepository
|
|||
$binds[] = [$beforeTs, PDO::PARAM_STR];
|
||||
$binds[] = [$beforeId, PDO::PARAM_INT];
|
||||
}
|
||||
$sql = 'SELECT ' . self::SELECT_COLUMNS . ' FROM events';
|
||||
$sql = 'SELECT ' . self::SELECT_COLUMNS . ' FROM move_events';
|
||||
if ($where) $sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
$sql .= ' ORDER BY occurred_at DESC, event_id DESC LIMIT ?';
|
||||
$binds[] = [$limit, PDO::PARAM_INT];
|
||||
|
|
@ -106,7 +106,7 @@ final class PdoEventRepository implements EventRepository
|
|||
|
||||
public function find(int $id): ?array
|
||||
{
|
||||
$st = $this->pdo->prepare('SELECT ' . self::SELECT_COLUMNS . ' FROM events WHERE event_id = ?');
|
||||
$st = $this->pdo->prepare('SELECT ' . self::SELECT_COLUMNS . ' FROM move_events WHERE event_id = ?');
|
||||
$st->execute([$id]);
|
||||
$row = $st->fetch();
|
||||
return $row === false ? null : self::mapRow($row);
|
||||
|
|
@ -119,13 +119,13 @@ final class PdoEventRepository implements EventRepository
|
|||
// "changed rows" rather than "matched rows": re-acknowledging an
|
||||
// already-acknowledged event by the same user would otherwise report
|
||||
// 0 affected rows and this method would wrongly return false.
|
||||
$exists = $this->pdo->prepare('SELECT 1 FROM events WHERE event_id = ? LIMIT 1');
|
||||
$exists = $this->pdo->prepare('SELECT 1 FROM move_events WHERE event_id = ? LIMIT 1');
|
||||
$exists->execute([$id]);
|
||||
if ($exists->fetchColumn() === false) {
|
||||
return false;
|
||||
}
|
||||
$st = $this->pdo->prepare(
|
||||
'UPDATE events SET acknowledged_by = ?, acknowledged_at = ? WHERE event_id = ?'
|
||||
'UPDATE move_events SET acknowledged_by = ?, acknowledged_at = ? WHERE event_id = ?'
|
||||
);
|
||||
$st->execute([$user, time(), $id]);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -20,20 +20,20 @@ final class PdoNonceRepository implements NonceRepository
|
|||
|
||||
public function seen(string $nonce): bool
|
||||
{
|
||||
$st = $this->pdo->prepare('SELECT 1 FROM nonces WHERE nonce = ? LIMIT 1');
|
||||
$st = $this->pdo->prepare('SELECT 1 FROM move_nonces WHERE nonce = ? LIMIT 1');
|
||||
$st->execute([$nonce]);
|
||||
return (bool)$st->fetchColumn();
|
||||
}
|
||||
|
||||
public function remember(string $nonce, int $expiresAt): void
|
||||
{
|
||||
$st = $this->pdo->prepare('INSERT INTO nonces (nonce, expires_at) VALUES (?, ?)');
|
||||
$st = $this->pdo->prepare('INSERT INTO move_nonces (nonce, expires_at) VALUES (?, ?)');
|
||||
$st->execute([$nonce, $expiresAt]);
|
||||
// Cron-less reap: Cafe24 has no scheduler, so expired nonces accumulate forever until
|
||||
// the table bloats and inserts slow/fail. Opportunistically prune a bounded batch of
|
||||
// already-expired rows (safe: seen() only cares about unexpired ones).
|
||||
if (mt_rand(1, 100) === 1) {
|
||||
$del = $this->pdo->prepare('DELETE FROM nonces WHERE expires_at < ? LIMIT 5000');
|
||||
$del = $this->pdo->prepare('DELETE FROM move_nonces WHERE expires_at < ? LIMIT 5000');
|
||||
$del->execute([time()]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ final class PdoRateLimiterRepository implements RateLimiterRepository
|
|||
public function hit(string $key, int $windowStart): int
|
||||
{
|
||||
$upsert = $this->pdo->prepare(
|
||||
'INSERT INTO rate_limits (rate_key, window_start, count) VALUES (?, ?, 1)
|
||||
'INSERT INTO move_rate_limits (rate_key, window_start, count) VALUES (?, ?, 1)
|
||||
ON DUPLICATE KEY UPDATE count = count + 1'
|
||||
);
|
||||
$upsert->execute([$key, $windowStart]);
|
||||
|
||||
$select = $this->pdo->prepare(
|
||||
'SELECT count FROM rate_limits WHERE rate_key = ? AND window_start = ?'
|
||||
'SELECT count FROM move_rate_limits WHERE rate_key = ? AND window_start = ?'
|
||||
);
|
||||
$select->execute([$key, $windowStart]);
|
||||
$count = (int)$select->fetchColumn();
|
||||
|
|
@ -37,7 +37,7 @@ final class PdoRateLimiterRepository implements RateLimiterRepository
|
|||
// count is already computed above; swallow reap failures and let the next hit retry.
|
||||
if (mt_rand(1, 100) === 1) {
|
||||
try {
|
||||
$del = $this->pdo->prepare('DELETE FROM rate_limits WHERE window_start < ? LIMIT 5000');
|
||||
$del = $this->pdo->prepare('DELETE FROM move_rate_limits WHERE window_start < ? LIMIT 5000');
|
||||
$del->execute([$windowStart - 2]);
|
||||
} catch (\PDOException $e) {
|
||||
error_log('rate_limits reap skipped: ' . $e->getMessage());
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
|||
public function add(int $eventId, string $fileUuid, string $contentHash): void
|
||||
{
|
||||
$st = $this->pdo->prepare(
|
||||
'INSERT INTO event_snapshots (event_id, file_uuid, content_hash, created_at)
|
||||
'INSERT INTO move_event_snapshots (event_id, file_uuid, content_hash, created_at)
|
||||
VALUES (?, ?, ?, ?)'
|
||||
);
|
||||
$st->execute([$eventId, $fileUuid, $contentHash, time()]);
|
||||
|
|
@ -21,7 +21,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
|||
public function forEvent(int $eventId): array
|
||||
{
|
||||
$st = $this->pdo->prepare(
|
||||
'SELECT file_uuid, content_hash FROM event_snapshots
|
||||
'SELECT file_uuid, content_hash FROM move_event_snapshots
|
||||
WHERE event_id = ? ORDER BY snapshot_id ASC'
|
||||
);
|
||||
$st->execute([$eventId]);
|
||||
|
|
@ -39,7 +39,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
|||
return null;
|
||||
}
|
||||
|
||||
$st = $this->pdo->prepare('SELECT 1 FROM event_snapshots WHERE file_uuid = ? LIMIT 1');
|
||||
$st = $this->pdo->prepare('SELECT 1 FROM move_event_snapshots WHERE file_uuid = ? LIMIT 1');
|
||||
$st->execute([$uuid]);
|
||||
if ($st->fetchColumn() === false) {
|
||||
return null;
|
||||
|
|
@ -53,7 +53,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
|||
if (strlen($uuid) !== 32 || !ctype_xdigit($uuid)) {
|
||||
return null;
|
||||
}
|
||||
$st = $this->pdo->prepare('SELECT event_id FROM event_snapshots WHERE file_uuid = ? LIMIT 1');
|
||||
$st = $this->pdo->prepare('SELECT event_id FROM move_event_snapshots WHERE file_uuid = ? LIMIT 1');
|
||||
$st->execute([$uuid]);
|
||||
$id = $st->fetchColumn();
|
||||
return $id === false ? null : (int)$id;
|
||||
|
|
@ -66,7 +66,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
|||
// row (file gone, row left) is harmless; an orphan file (row gone, file left) is the
|
||||
// PIPA leak we are closing, so never delete the row before its file.
|
||||
$sel = $this->pdo->prepare(
|
||||
'SELECT snapshot_id, file_uuid FROM event_snapshots WHERE created_at < ? LIMIT ?');
|
||||
'SELECT snapshot_id, file_uuid FROM move_event_snapshots WHERE created_at < ? LIMIT ?');
|
||||
$sel->bindValue(1, $cutoff, PDO::PARAM_INT);
|
||||
$sel->bindValue(2, $limit, PDO::PARAM_INT);
|
||||
$sel->execute();
|
||||
|
|
@ -92,7 +92,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
|||
return 0;
|
||||
}
|
||||
$in = implode(',', array_fill(0, count($ids), '?'));
|
||||
$del = $this->pdo->prepare("DELETE FROM event_snapshots WHERE snapshot_id IN ($in)");
|
||||
$del = $this->pdo->prepare("DELETE FROM move_event_snapshots WHERE snapshot_id IN ($in)");
|
||||
$del->execute($ids);
|
||||
return count($ids);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ final class PdoThresholdRepository implements ThresholdRepository
|
|||
|
||||
public function forDevice(string $deviceId): array
|
||||
{
|
||||
$st = $this->pdo->prepare('SELECT `key`, `value` FROM thresholds WHERE device_id = ?');
|
||||
$st = $this->pdo->prepare('SELECT `key`, `value` FROM move_thresholds WHERE device_id = ?');
|
||||
$st->execute([$deviceId]);
|
||||
|
||||
$out = [];
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ final class PdoUserRepository implements UserRepository
|
|||
{
|
||||
$st = $this->pdo->prepare(
|
||||
'SELECT id, username, password_hash, role, site_scope, failed_attempts, locked_until
|
||||
FROM users WHERE username = ? LIMIT 1'
|
||||
FROM move_users WHERE username = ? LIMIT 1'
|
||||
);
|
||||
$st->execute([$username]);
|
||||
$row = $st->fetch();
|
||||
|
|
@ -36,14 +36,14 @@ final class PdoUserRepository implements UserRepository
|
|||
// Increment in SQL (not read-then-write in PHP) so concurrent failed
|
||||
// logins for the same account can't clobber each other's count.
|
||||
$st = $this->pdo->prepare(
|
||||
'UPDATE users SET failed_attempts = failed_attempts + 1, locked_until = ? WHERE id = ?'
|
||||
'UPDATE move_users SET failed_attempts = failed_attempts + 1, locked_until = ? WHERE id = ?'
|
||||
);
|
||||
$st->execute([$lockUntil, $userId]);
|
||||
}
|
||||
|
||||
public function clearFailures(int $userId): void
|
||||
{
|
||||
$st = $this->pdo->prepare('UPDATE users SET failed_attempts = 0, locked_until = 0 WHERE id = ?');
|
||||
$st = $this->pdo->prepare('UPDATE move_users SET failed_attempts = 0, locked_until = 0 WHERE id = ?');
|
||||
$st->execute([$userId]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ final class PdoZoneRepository implements ZoneRepository
|
|||
public function find(string $zoneId): ?array
|
||||
{
|
||||
$st = $this->pdo->prepare(
|
||||
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM zones WHERE zone_id = ? LIMIT 1'
|
||||
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM move_zones WHERE zone_id = ? LIMIT 1'
|
||||
);
|
||||
$st->execute([$zoneId]);
|
||||
$row = $st->fetch();
|
||||
|
|
@ -23,7 +23,7 @@ final class PdoZoneRepository implements ZoneRepository
|
|||
public function forDevice(string $deviceId): array
|
||||
{
|
||||
$st = $this->pdo->prepare(
|
||||
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM zones WHERE device_id = ?'
|
||||
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM move_zones WHERE device_id = ?'
|
||||
);
|
||||
$st->execute([$deviceId]);
|
||||
return $st->fetchAll();
|
||||
|
|
@ -32,7 +32,7 @@ final class PdoZoneRepository implements ZoneRepository
|
|||
public function forCamera(string $cameraId): array
|
||||
{
|
||||
$st = $this->pdo->prepare(
|
||||
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM zones WHERE camera_id = ?'
|
||||
'SELECT zone_id, device_id, camera_id, level, polygon_json FROM move_zones WHERE camera_id = ?'
|
||||
);
|
||||
$st->execute([$cameraId]);
|
||||
return $st->fetchAll();
|
||||
|
|
@ -52,7 +52,7 @@ final class PdoZoneRepository implements ZoneRepository
|
|||
|
||||
$newId = bin2hex(random_bytes(16));
|
||||
$st = $this->pdo->prepare(
|
||||
'INSERT INTO zones (zone_id, device_id, camera_id, level, polygon_json, updated_at)
|
||||
'INSERT INTO move_zones (zone_id, device_id, camera_id, level, polygon_json, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$st->execute([$newId, $deviceId, $cameraId, $level, $polygonJson, time()]);
|
||||
|
|
@ -61,7 +61,7 @@ final class PdoZoneRepository implements ZoneRepository
|
|||
|
||||
public function delete(string $zoneId): bool
|
||||
{
|
||||
$st = $this->pdo->prepare('DELETE FROM zones WHERE zone_id = ?');
|
||||
$st = $this->pdo->prepare('DELETE FROM move_zones WHERE zone_id = ?');
|
||||
$st->execute([$zoneId]);
|
||||
return $st->rowCount() > 0;
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ final class PdoZoneRepository implements ZoneRepository
|
|||
string $polygonJson,
|
||||
): string {
|
||||
$owns = $this->pdo->prepare(
|
||||
'SELECT 1 FROM zones WHERE zone_id = ? AND device_id = ? AND camera_id = ? LIMIT 1'
|
||||
'SELECT 1 FROM move_zones WHERE zone_id = ? AND device_id = ? AND camera_id = ? LIMIT 1'
|
||||
);
|
||||
$owns->execute([$zoneId, $deviceId, $cameraId]);
|
||||
if ($owns->fetchColumn() === false) {
|
||||
|
|
@ -97,7 +97,7 @@ final class PdoZoneRepository implements ZoneRepository
|
|||
}
|
||||
|
||||
$st = $this->pdo->prepare(
|
||||
'UPDATE zones SET level = ?, polygon_json = ?, updated_at = ?
|
||||
'UPDATE move_zones SET level = ?, polygon_json = ?, updated_at = ?
|
||||
WHERE zone_id = ? AND device_id = ? AND camera_id = ?'
|
||||
);
|
||||
$st->execute([$level, $polygonJson, time(), $zoneId, $deviceId, $cameraId]);
|
||||
|
|
@ -108,7 +108,7 @@ final class PdoZoneRepository implements ZoneRepository
|
|||
// error) or (b) the zone was deleted between the ownership check
|
||||
// above and this UPDATE. Disambiguate with a targeted existence
|
||||
// check instead of assuming either case.
|
||||
$stillExists = $this->pdo->prepare('SELECT 1 FROM zones WHERE zone_id = ? LIMIT 1');
|
||||
$stillExists = $this->pdo->prepare('SELECT 1 FROM move_zones WHERE zone_id = ? LIMIT 1');
|
||||
$stillExists->execute([$zoneId]);
|
||||
if ($stillExists->fetchColumn() === false) {
|
||||
throw new ZoneOwnershipException('zone was deleted concurrently');
|
||||
|
|
|
|||
|
|
@ -148,12 +148,12 @@ final class PdoIntegrationTest extends TestCase
|
|||
private static function sqliteSchema(): string
|
||||
{
|
||||
return <<<'SQL'
|
||||
CREATE TABLE nonces (
|
||||
CREATE TABLE move_nonces (
|
||||
nonce TEXT PRIMARY KEY,
|
||||
expires_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE events (
|
||||
CREATE TABLE move_events (
|
||||
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_uuid TEXT UNIQUE,
|
||||
device_id TEXT NOT NULL,
|
||||
|
|
@ -171,7 +171,7 @@ final class PdoIntegrationTest extends TestCase
|
|||
received_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE zones (
|
||||
CREATE TABLE move_zones (
|
||||
zone_id TEXT PRIMARY KEY,
|
||||
device_id TEXT NOT NULL,
|
||||
camera_id TEXT NOT NULL,
|
||||
|
|
|
|||
Loading…
Reference in a new issue