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 탭:
|
phpMyAdmin → SQL 탭:
|
||||||
|
|
||||||
```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);
|
VALUES ('admin', '여기에_해시_붙여넣기', 'admin', NULL, 0, 0);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -144,7 +144,7 @@ php -r "echo bin2hex(random_bytes(32)), \"\n\";"
|
||||||
```
|
```
|
||||||
|
|
||||||
```sql
|
```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);
|
VALUES ('pi-01', '3라인 낙상 카메라', '여기에_비밀키_붙여넣기', 1);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,14 +188,14 @@ php -r "echo bin2hex(random_bytes(32)), \"\n\";"
|
||||||
```
|
```
|
||||||
|
|
||||||
```sql
|
```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);
|
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
|
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
|
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
|
`/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.
|
old value stops working immediately since `secretFor()` always reads the current row.
|
||||||
|
|
||||||
## 7. First admin account
|
## 7. First admin account
|
||||||
|
|
@ -215,7 +215,7 @@ php -r "echo password_hash('CHANGE-ME-to-a-real-passphrase', PASSWORD_ARGON2ID),
|
||||||
`PASSWORD_ARGON2ID`.)
|
`PASSWORD_ARGON2ID`.)
|
||||||
|
|
||||||
```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', '<paste the argon2id hash here>', 'admin', NULL, 0, 0);
|
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.
|
-- 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 COLUMN site VARCHAR(64) NULL AFTER name,
|
||||||
ADD KEY ix_devices_site (site);
|
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.
|
-- site-scoped event list.
|
||||||
ALTER TABLE events
|
ALTER TABLE move_events
|
||||||
ADD COLUMN site VARCHAR(64) NULL AFTER device_id,
|
ADD COLUMN site VARCHAR(64) NULL AFTER device_id,
|
||||||
ADD KEY ix_events_site_occurred_at (site, occurred_at);
|
ADD KEY ix_events_site_occurred_at (site, occurred_at);
|
||||||
|
|
||||||
-- A3) events.event_uuid — edge-generated idempotency key (P1-9). The UNIQUE key
|
-- 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
|
-- 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.
|
-- 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 COLUMN event_uuid CHAR(32) NULL AFTER event_id,
|
||||||
ADD UNIQUE KEY uq_events_uuid (event_uuid);
|
ADD UNIQUE KEY uq_events_uuid (event_uuid);
|
||||||
|
|
||||||
-- A4) event_snapshots.created_at index — PdoSnapshotRepository::purgeOlderThan()
|
-- A4) event_snapshots.created_at index — PdoSnapshotRepository::purgeOlderThan()
|
||||||
-- runs "SELECT ... WHERE created_at < ?" inside live ingest requests. Without
|
-- runs "SELECT ... WHERE created_at < ?" inside live ingest requests. Without
|
||||||
-- this index it full-scans the snapshot table on every triggered purge.
|
-- 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);
|
ADD KEY ix_event_snapshots_created (created_at);
|
||||||
|
|
||||||
-- A5) event_snapshots (event_id, content_hash) UNIQUE — lets the ingest dedup path
|
-- 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
|
-- collides instead of writing a second copy). If this fails on "Duplicate
|
||||||
-- entry", the table already has duplicate (event_id, content_hash) rows —
|
-- entry", the table already has duplicate (event_id, content_hash) rows —
|
||||||
-- remove the dups, then re-run this one statement.
|
-- 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);
|
ADD UNIQUE KEY uq_event_snapshots_event_hash (event_id, content_hash);
|
||||||
|
|
||||||
-- A6) rate_limits.window_start index — the reap "DELETE ... WHERE window_start < ?"
|
-- 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
|
-- 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.
|
-- 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);
|
ADD KEY ix_rate_limits_window (window_start);
|
||||||
|
|
||||||
-- ===========================================================================
|
-- ===========================================================================
|
||||||
-- PART B — provisioning (EDIT the values for your deployment, then run).
|
-- 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.
|
-- 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 move_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-b' WHERE device_id = 'pi-2';
|
||||||
-- -- ... one row per device ...
|
-- -- ... 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
|
-- 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.
|
-- device's site so its past events reappear for scoped operators.
|
||||||
-- ===========================================================================
|
-- ===========================================================================
|
||||||
UPDATE events e
|
UPDATE move_events e
|
||||||
JOIN devices d ON d.device_id = e.device_id
|
JOIN move_devices d ON d.device_id = e.device_id
|
||||||
SET e.site = d.site
|
SET e.site = d.site
|
||||||
WHERE e.site IS NULL AND d.site IS NOT NULL;
|
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
|
-- see their backfilled history). NULL = global operator; '' is NOT
|
||||||
-- global (matches nothing — an accidental blank fails closed).
|
-- 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) -----------
|
-- ---- Verification (run these; each must confirm the object/state) -----------
|
||||||
-- SHOW COLUMNS FROM devices LIKE 'site';
|
-- SHOW COLUMNS FROM move_devices LIKE 'site';
|
||||||
-- SHOW COLUMNS FROM events LIKE 'site';
|
-- SHOW COLUMNS FROM move_events LIKE 'site';
|
||||||
-- SHOW COLUMNS FROM events LIKE 'event_uuid';
|
-- SHOW COLUMNS FROM move_events LIKE 'event_uuid';
|
||||||
-- SHOW INDEX FROM events WHERE Key_name = 'uq_events_uuid';
|
-- SHOW INDEX FROM move_events WHERE Key_name = 'uq_events_uuid';
|
||||||
-- SHOW INDEX FROM event_snapshots WHERE Key_name = 'ix_event_snapshots_created';
|
-- SHOW INDEX FROM move_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 move_event_snapshots WHERE Key_name = 'uq_event_snapshots_event_hash';
|
||||||
-- SHOW INDEX FROM rate_limits WHERE Key_name = 'ix_rate_limits_window';
|
-- 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
|
-- 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):
|
-- 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;
|
-- 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
|
-- 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.
|
-- 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,
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
username VARCHAR(190) NOT NULL,
|
username VARCHAR(190) NOT NULL,
|
||||||
password_hash VARCHAR(255) NOT NULL, -- Argon2id, see App\Security\Password::hash()
|
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
|
-- No PdoDeviceRepository::create() exists -- rows are provisioned by an admin
|
||||||
-- (per-device secret issuance) before the device is allowed to sync/ingest.
|
-- (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,
|
device_id VARCHAR(64) NOT NULL,
|
||||||
name VARCHAR(190) NULL,
|
name VARCHAR(190) NULL,
|
||||||
site VARCHAR(64) NULL, -- access-control scope; matches users.site_scope (NULL = unassigned)
|
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
|
-- directly as a free-form string. Kept for documentation/future per-camera
|
||||||
-- config and to avoid a breaking migration when that repository is added.
|
-- 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,
|
camera_id VARCHAR(64) NOT NULL,
|
||||||
device_id VARCHAR(64) NOT NULL,
|
device_id VARCHAR(64) NOT NULL,
|
||||||
name VARCHAR(190) 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
|
-- `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.
|
-- 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
|
zone_id CHAR(32) NOT NULL, -- bin2hex(random_bytes(16)), same convention as snapshot uuids
|
||||||
device_id VARCHAR(64) NOT NULL,
|
device_id VARCHAR(64) NOT NULL,
|
||||||
camera_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
|
-- App\Ingest\EventValidator before insert(); the CHECK constraints below are
|
||||||
-- defense-in-depth only, not the primary validation layer.
|
-- 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_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
|
event_uuid CHAR(32) NULL, -- edge-generated idempotency key (dedup a re-sent event); NULL for legacy rows
|
||||||
device_id VARCHAR(64) NOT NULL,
|
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,
|
camera_id VARCHAR(64) NOT NULL,
|
||||||
event_type VARCHAR(32) NOT NULL,
|
event_type VARCHAR(32) NOT NULL,
|
||||||
severity VARCHAR(16) 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
|
-- events is safe because IngestController always inserts the parent event row
|
||||||
-- before calling SnapshotRepository::add() for its snapshots.
|
-- before calling SnapshotRepository::add() for its snapshots.
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
CREATE TABLE event_snapshots (
|
CREATE TABLE move_event_snapshots (
|
||||||
snapshot_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
snapshot_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
event_id INT UNSIGNED NOT NULL,
|
event_id INT UNSIGNED NOT NULL,
|
||||||
file_uuid CHAR(32) NOT NULL, -- bin2hex(random_bytes(16)); on-disk filename = <uuid>.jpg
|
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),
|
UNIQUE KEY uq_event_snapshots_event_hash (event_id, content_hash),
|
||||||
KEY ix_event_snapshots_event (event_id),
|
KEY ix_event_snapshots_event (event_id),
|
||||||
KEY ix_event_snapshots_created (created_at), -- retention purge scans WHERE created_at < ?
|
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
|
ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
|
@ -173,7 +173,7 @@ CREATE TABLE event_snapshots (
|
||||||
-- GET /api/config. No PdoThresholdRepository write method exists in P1-B --
|
-- GET /api/config. No PdoThresholdRepository write method exists in P1-B --
|
||||||
-- rows are seeded/edited directly (phpMyAdmin) until a management UI lands.
|
-- 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,
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
device_id VARCHAR(64) NOT NULL,
|
device_id VARCHAR(64) NOT NULL,
|
||||||
camera_id VARCHAR(64) 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,
|
-- TOCTOU flagged in the Task 7 review -- the PK uniqueness is the real guard,
|
||||||
-- not the earlier seen() check).
|
-- not the earlier seen() check).
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
CREATE TABLE nonces (
|
CREATE TABLE move_nonces (
|
||||||
nonce VARCHAR(190) NOT NULL,
|
nonce VARCHAR(190) NOT NULL,
|
||||||
expires_at BIGINT UNSIGNED NOT NULL,
|
expires_at BIGINT UNSIGNED NOT NULL,
|
||||||
PRIMARY KEY (nonce),
|
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;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
@ -207,7 +207,7 @@ CREATE TABLE nonces (
|
||||||
-- `INSERT ... ON DUPLICATE KEY UPDATE count = count + 1` upsert in
|
-- `INSERT ... ON DUPLICATE KEY UPDATE count = count + 1` upsert in
|
||||||
-- PdoRateLimiterRepository::hit() -- no read-modify-write race window.
|
-- PdoRateLimiterRepository::hit() -- no read-modify-write race window.
|
||||||
-- ---------------------------------------------------------------------------
|
-- ---------------------------------------------------------------------------
|
||||||
CREATE TABLE rate_limits (
|
CREATE TABLE move_rate_limits (
|
||||||
rate_key VARCHAR(190) NOT NULL,
|
rate_key VARCHAR(190) NOT NULL,
|
||||||
window_start BIGINT UNSIGNED NOT NULL, -- floor(now / 60): the minute bucket
|
window_start BIGINT UNSIGNED NOT NULL, -- floor(now / 60): the minute bucket
|
||||||
count INT UNSIGNED NOT NULL DEFAULT 0,
|
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
|
-- 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.
|
-- 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,
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
actor VARCHAR(190) NOT NULL,
|
actor VARCHAR(190) NOT NULL,
|
||||||
action VARCHAR(64) NOT NULL,
|
action VARCHAR(64) NOT NULL,
|
||||||
|
|
@ -241,13 +241,18 @@ CREATE TABLE audit_log (
|
||||||
-- run. Apply the RUNNABLE migration instead:
|
-- run. Apply the RUNNABLE migration instead:
|
||||||
--
|
--
|
||||||
-- cloud/migrations/2026-07-17-p1-9-p3-2b.sql
|
-- 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
|
-- 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
|
-- that backfill, historical events vanish for a scoped operator). Run it once in
|
||||||
-- phpMyAdmin BEFORE deploying code that reads these columns, then provision:
|
-- phpMyAdmin BEFORE deploying code that reads these columns, then provision:
|
||||||
-- UPDATE devices SET site = 'factory-a' WHERE device_id = 'pi-1';
|
-- UPDATE move_devices SET site = 'factory-a' WHERE device_id = 'pi-1';
|
||||||
-- UPDATE users SET site_scope = 'factory-a' WHERE username = 'operator-a';
|
-- 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
|
-- A user with site_scope = NULL is a global operator; '' is NOT global (matches
|
||||||
-- nothing — a blank scope fails closed).
|
-- 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
|
public function append(string $actor, string $action, string $target, array $before, array $after): void
|
||||||
{
|
{
|
||||||
$st = $this->pdo->prepare(
|
$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 (?, ?, ?, ?, ?, ?)'
|
VALUES (?, ?, ?, ?, ?, ?)'
|
||||||
);
|
);
|
||||||
$st->execute([
|
$st->execute([
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ final class PdoDeviceRepository implements DeviceRepository
|
||||||
|
|
||||||
public function secretFor(string $deviceId): ?string
|
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]);
|
$st->execute([$deviceId]);
|
||||||
$secret = $st->fetchColumn();
|
$secret = $st->fetchColumn();
|
||||||
return $secret === false ? null : (string)$secret;
|
return $secret === false ? null : (string)$secret;
|
||||||
|
|
@ -19,7 +19,7 @@ final class PdoDeviceRepository implements DeviceRepository
|
||||||
|
|
||||||
public function siteFor(string $deviceId): ?string
|
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]);
|
$st->execute([$deviceId]);
|
||||||
$site = $st->fetchColumn();
|
$site = $st->fetchColumn();
|
||||||
// false = no such device; null = device exists but unassigned. Both -> null (unscoped).
|
// 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
|
// out-of-band by an admin (see cloud/docs/cloud-deploy.md), never created
|
||||||
// implicitly from an inbound heartbeat.
|
// implicitly from an inbound heartbeat.
|
||||||
$st = $this->pdo->prepare(
|
$st = $this->pdo->prepare(
|
||||||
'UPDATE devices
|
'UPDATE move_devices
|
||||||
SET last_fps = ?, last_temp = ?, last_throttle = ?,
|
SET last_fps = ?, last_temp = ?, last_throttle = ?,
|
||||||
reported_config_version = ?, last_heartbeat_at = ?
|
reported_config_version = ?, last_heartbeat_at = ?
|
||||||
WHERE device_id = ?'
|
WHERE device_id = ?'
|
||||||
|
|
@ -51,7 +51,7 @@ final class PdoDeviceRepository implements DeviceRepository
|
||||||
|
|
||||||
public function configVersion(string $deviceId): int
|
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]);
|
$st->execute([$deviceId]);
|
||||||
$version = $st->fetchColumn();
|
$version = $st->fetchColumn();
|
||||||
return $version === false ? 1 : (int)$version;
|
return $version === false ? 1 : (int)$version;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ final class PdoEventRepository implements EventRepository
|
||||||
public function insert(array $event): int
|
public function insert(array $event): int
|
||||||
{
|
{
|
||||||
$st = $this->pdo->prepare(
|
$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,
|
(event_uuid, device_id, site, camera_id, event_type, severity, track_id, zone_id,
|
||||||
occurred_at, duration_s, metrics_json, received_at)
|
occurred_at, duration_s, metrics_json, received_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||||
|
|
@ -24,7 +24,7 @@ final class PdoEventRepository implements EventRepository
|
||||||
$st->execute([
|
$st->execute([
|
||||||
$event['event_uuid'] ?? null, // NULL when the edge sent no idempotency key (legacy)
|
$event['event_uuid'] ?? null, // NULL when the edge sent no idempotency key (legacy)
|
||||||
(string)$event['device_id'],
|
(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['camera_id'],
|
||||||
(string)$event['event_type'],
|
(string)$event['event_type'],
|
||||||
(string)$event['severity'],
|
(string)$event['severity'],
|
||||||
|
|
@ -40,7 +40,7 @@ final class PdoEventRepository implements EventRepository
|
||||||
|
|
||||||
public function idByUuid(string $uuid): ?int
|
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]);
|
$st->execute([$uuid]);
|
||||||
$id = $st->fetchColumn();
|
$id = $st->fetchColumn();
|
||||||
return $id === false ? null : (int)$id;
|
return $id === false ? null : (int)$id;
|
||||||
|
|
@ -51,7 +51,7 @@ final class PdoEventRepository implements EventRepository
|
||||||
if ($severity !== null) {
|
if ($severity !== null) {
|
||||||
$st = $this->pdo->prepare(
|
$st = $this->pdo->prepare(
|
||||||
'SELECT ' . self::SELECT_COLUMNS . '
|
'SELECT ' . self::SELECT_COLUMNS . '
|
||||||
FROM events WHERE severity = ?
|
FROM move_events WHERE severity = ?
|
||||||
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
|
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
|
||||||
);
|
);
|
||||||
$st->bindValue(1, $severity, PDO::PARAM_STR);
|
$st->bindValue(1, $severity, PDO::PARAM_STR);
|
||||||
|
|
@ -59,7 +59,7 @@ final class PdoEventRepository implements EventRepository
|
||||||
} else {
|
} else {
|
||||||
$st = $this->pdo->prepare(
|
$st = $this->pdo->prepare(
|
||||||
'SELECT ' . self::SELECT_COLUMNS . '
|
'SELECT ' . self::SELECT_COLUMNS . '
|
||||||
FROM events
|
FROM move_events
|
||||||
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
|
ORDER BY occurred_at DESC, event_id DESC LIMIT ?'
|
||||||
);
|
);
|
||||||
$st->bindValue(1, $limit, PDO::PARAM_INT);
|
$st->bindValue(1, $limit, PDO::PARAM_INT);
|
||||||
|
|
@ -92,7 +92,7 @@ final class PdoEventRepository implements EventRepository
|
||||||
$binds[] = [$beforeTs, PDO::PARAM_STR];
|
$binds[] = [$beforeTs, PDO::PARAM_STR];
|
||||||
$binds[] = [$beforeId, PDO::PARAM_INT];
|
$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);
|
if ($where) $sql .= ' WHERE ' . implode(' AND ', $where);
|
||||||
$sql .= ' ORDER BY occurred_at DESC, event_id DESC LIMIT ?';
|
$sql .= ' ORDER BY occurred_at DESC, event_id DESC LIMIT ?';
|
||||||
$binds[] = [$limit, PDO::PARAM_INT];
|
$binds[] = [$limit, PDO::PARAM_INT];
|
||||||
|
|
@ -106,7 +106,7 @@ final class PdoEventRepository implements EventRepository
|
||||||
|
|
||||||
public function find(int $id): ?array
|
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]);
|
$st->execute([$id]);
|
||||||
$row = $st->fetch();
|
$row = $st->fetch();
|
||||||
return $row === false ? null : self::mapRow($row);
|
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
|
// "changed rows" rather than "matched rows": re-acknowledging an
|
||||||
// already-acknowledged event by the same user would otherwise report
|
// already-acknowledged event by the same user would otherwise report
|
||||||
// 0 affected rows and this method would wrongly return false.
|
// 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]);
|
$exists->execute([$id]);
|
||||||
if ($exists->fetchColumn() === false) {
|
if ($exists->fetchColumn() === false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$st = $this->pdo->prepare(
|
$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]);
|
$st->execute([$user, time(), $id]);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -20,20 +20,20 @@ final class PdoNonceRepository implements NonceRepository
|
||||||
|
|
||||||
public function seen(string $nonce): bool
|
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]);
|
$st->execute([$nonce]);
|
||||||
return (bool)$st->fetchColumn();
|
return (bool)$st->fetchColumn();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function remember(string $nonce, int $expiresAt): void
|
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]);
|
$st->execute([$nonce, $expiresAt]);
|
||||||
// Cron-less reap: Cafe24 has no scheduler, so expired nonces accumulate forever until
|
// 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
|
// the table bloats and inserts slow/fail. Opportunistically prune a bounded batch of
|
||||||
// already-expired rows (safe: seen() only cares about unexpired ones).
|
// already-expired rows (safe: seen() only cares about unexpired ones).
|
||||||
if (mt_rand(1, 100) === 1) {
|
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()]);
|
$del->execute([time()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,13 @@ final class PdoRateLimiterRepository implements RateLimiterRepository
|
||||||
public function hit(string $key, int $windowStart): int
|
public function hit(string $key, int $windowStart): int
|
||||||
{
|
{
|
||||||
$upsert = $this->pdo->prepare(
|
$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'
|
ON DUPLICATE KEY UPDATE count = count + 1'
|
||||||
);
|
);
|
||||||
$upsert->execute([$key, $windowStart]);
|
$upsert->execute([$key, $windowStart]);
|
||||||
|
|
||||||
$select = $this->pdo->prepare(
|
$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]);
|
$select->execute([$key, $windowStart]);
|
||||||
$count = (int)$select->fetchColumn();
|
$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.
|
// count is already computed above; swallow reap failures and let the next hit retry.
|
||||||
if (mt_rand(1, 100) === 1) {
|
if (mt_rand(1, 100) === 1) {
|
||||||
try {
|
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]);
|
$del->execute([$windowStart - 2]);
|
||||||
} catch (\PDOException $e) {
|
} catch (\PDOException $e) {
|
||||||
error_log('rate_limits reap skipped: ' . $e->getMessage());
|
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
|
public function add(int $eventId, string $fileUuid, string $contentHash): void
|
||||||
{
|
{
|
||||||
$st = $this->pdo->prepare(
|
$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 (?, ?, ?, ?)'
|
VALUES (?, ?, ?, ?)'
|
||||||
);
|
);
|
||||||
$st->execute([$eventId, $fileUuid, $contentHash, time()]);
|
$st->execute([$eventId, $fileUuid, $contentHash, time()]);
|
||||||
|
|
@ -21,7 +21,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
||||||
public function forEvent(int $eventId): array
|
public function forEvent(int $eventId): array
|
||||||
{
|
{
|
||||||
$st = $this->pdo->prepare(
|
$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'
|
WHERE event_id = ? ORDER BY snapshot_id ASC'
|
||||||
);
|
);
|
||||||
$st->execute([$eventId]);
|
$st->execute([$eventId]);
|
||||||
|
|
@ -39,7 +39,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
||||||
return null;
|
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]);
|
$st->execute([$uuid]);
|
||||||
if ($st->fetchColumn() === false) {
|
if ($st->fetchColumn() === false) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -53,7 +53,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
||||||
if (strlen($uuid) !== 32 || !ctype_xdigit($uuid)) {
|
if (strlen($uuid) !== 32 || !ctype_xdigit($uuid)) {
|
||||||
return null;
|
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]);
|
$st->execute([$uuid]);
|
||||||
$id = $st->fetchColumn();
|
$id = $st->fetchColumn();
|
||||||
return $id === false ? null : (int)$id;
|
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
|
// 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.
|
// PIPA leak we are closing, so never delete the row before its file.
|
||||||
$sel = $this->pdo->prepare(
|
$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(1, $cutoff, PDO::PARAM_INT);
|
||||||
$sel->bindValue(2, $limit, PDO::PARAM_INT);
|
$sel->bindValue(2, $limit, PDO::PARAM_INT);
|
||||||
$sel->execute();
|
$sel->execute();
|
||||||
|
|
@ -92,7 +92,7 @@ final class PdoSnapshotRepository implements SnapshotRepository
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$in = implode(',', array_fill(0, count($ids), '?'));
|
$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);
|
$del->execute($ids);
|
||||||
return count($ids);
|
return count($ids);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ final class PdoThresholdRepository implements ThresholdRepository
|
||||||
|
|
||||||
public function forDevice(string $deviceId): array
|
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]);
|
$st->execute([$deviceId]);
|
||||||
|
|
||||||
$out = [];
|
$out = [];
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ final class PdoUserRepository implements UserRepository
|
||||||
{
|
{
|
||||||
$st = $this->pdo->prepare(
|
$st = $this->pdo->prepare(
|
||||||
'SELECT id, username, password_hash, role, site_scope, failed_attempts, locked_until
|
'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]);
|
$st->execute([$username]);
|
||||||
$row = $st->fetch();
|
$row = $st->fetch();
|
||||||
|
|
@ -36,14 +36,14 @@ final class PdoUserRepository implements UserRepository
|
||||||
// Increment in SQL (not read-then-write in PHP) so concurrent failed
|
// Increment in SQL (not read-then-write in PHP) so concurrent failed
|
||||||
// logins for the same account can't clobber each other's count.
|
// logins for the same account can't clobber each other's count.
|
||||||
$st = $this->pdo->prepare(
|
$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]);
|
$st->execute([$lockUntil, $userId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clearFailures(int $userId): void
|
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]);
|
$st->execute([$userId]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ final class PdoZoneRepository implements ZoneRepository
|
||||||
public function find(string $zoneId): ?array
|
public function find(string $zoneId): ?array
|
||||||
{
|
{
|
||||||
$st = $this->pdo->prepare(
|
$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]);
|
$st->execute([$zoneId]);
|
||||||
$row = $st->fetch();
|
$row = $st->fetch();
|
||||||
|
|
@ -23,7 +23,7 @@ final class PdoZoneRepository implements ZoneRepository
|
||||||
public function forDevice(string $deviceId): array
|
public function forDevice(string $deviceId): array
|
||||||
{
|
{
|
||||||
$st = $this->pdo->prepare(
|
$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]);
|
$st->execute([$deviceId]);
|
||||||
return $st->fetchAll();
|
return $st->fetchAll();
|
||||||
|
|
@ -32,7 +32,7 @@ final class PdoZoneRepository implements ZoneRepository
|
||||||
public function forCamera(string $cameraId): array
|
public function forCamera(string $cameraId): array
|
||||||
{
|
{
|
||||||
$st = $this->pdo->prepare(
|
$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]);
|
$st->execute([$cameraId]);
|
||||||
return $st->fetchAll();
|
return $st->fetchAll();
|
||||||
|
|
@ -52,7 +52,7 @@ final class PdoZoneRepository implements ZoneRepository
|
||||||
|
|
||||||
$newId = bin2hex(random_bytes(16));
|
$newId = bin2hex(random_bytes(16));
|
||||||
$st = $this->pdo->prepare(
|
$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 (?, ?, ?, ?, ?, ?)'
|
VALUES (?, ?, ?, ?, ?, ?)'
|
||||||
);
|
);
|
||||||
$st->execute([$newId, $deviceId, $cameraId, $level, $polygonJson, time()]);
|
$st->execute([$newId, $deviceId, $cameraId, $level, $polygonJson, time()]);
|
||||||
|
|
@ -61,7 +61,7 @@ final class PdoZoneRepository implements ZoneRepository
|
||||||
|
|
||||||
public function delete(string $zoneId): bool
|
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]);
|
$st->execute([$zoneId]);
|
||||||
return $st->rowCount() > 0;
|
return $st->rowCount() > 0;
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +89,7 @@ final class PdoZoneRepository implements ZoneRepository
|
||||||
string $polygonJson,
|
string $polygonJson,
|
||||||
): string {
|
): string {
|
||||||
$owns = $this->pdo->prepare(
|
$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]);
|
$owns->execute([$zoneId, $deviceId, $cameraId]);
|
||||||
if ($owns->fetchColumn() === false) {
|
if ($owns->fetchColumn() === false) {
|
||||||
|
|
@ -97,7 +97,7 @@ final class PdoZoneRepository implements ZoneRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
$st = $this->pdo->prepare(
|
$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 = ?'
|
WHERE zone_id = ? AND device_id = ? AND camera_id = ?'
|
||||||
);
|
);
|
||||||
$st->execute([$level, $polygonJson, time(), $zoneId, $deviceId, $cameraId]);
|
$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
|
// error) or (b) the zone was deleted between the ownership check
|
||||||
// above and this UPDATE. Disambiguate with a targeted existence
|
// above and this UPDATE. Disambiguate with a targeted existence
|
||||||
// check instead of assuming either case.
|
// 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]);
|
$stillExists->execute([$zoneId]);
|
||||||
if ($stillExists->fetchColumn() === false) {
|
if ($stillExists->fetchColumn() === false) {
|
||||||
throw new ZoneOwnershipException('zone was deleted concurrently');
|
throw new ZoneOwnershipException('zone was deleted concurrently');
|
||||||
|
|
|
||||||
|
|
@ -148,12 +148,12 @@ final class PdoIntegrationTest extends TestCase
|
||||||
private static function sqliteSchema(): string
|
private static function sqliteSchema(): string
|
||||||
{
|
{
|
||||||
return <<<'SQL'
|
return <<<'SQL'
|
||||||
CREATE TABLE nonces (
|
CREATE TABLE move_nonces (
|
||||||
nonce TEXT PRIMARY KEY,
|
nonce TEXT PRIMARY KEY,
|
||||||
expires_at INTEGER NOT NULL
|
expires_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE events (
|
CREATE TABLE move_events (
|
||||||
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
event_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
event_uuid TEXT UNIQUE,
|
event_uuid TEXT UNIQUE,
|
||||||
device_id TEXT NOT NULL,
|
device_id TEXT NOT NULL,
|
||||||
|
|
@ -171,7 +171,7 @@ final class PdoIntegrationTest extends TestCase
|
||||||
received_at INTEGER NOT NULL
|
received_at INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE zones (
|
CREATE TABLE move_zones (
|
||||||
zone_id TEXT PRIMARY KEY,
|
zone_id TEXT PRIMARY KEY,
|
||||||
device_id TEXT NOT NULL,
|
device_id TEXT NOT NULL,
|
||||||
camera_id TEXT NOT NULL,
|
camera_id TEXT NOT NULL,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue