diff --git a/cloud/deploy/README-UPLOAD.md b/cloud/deploy/README-UPLOAD.md index c248956..6d1fd4a 100644 --- a/cloud/deploy/README-UPLOAD.md +++ b/cloud/deploy/README-UPLOAD.md @@ -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); ``` diff --git a/cloud/docs/cloud-deploy.md b/cloud/docs/cloud-deploy.md index 89d7322..9e23ac7 100644 --- a/cloud/docs/cloud-deploy.md +++ b/cloud/docs/cloud-deploy.md @@ -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', '', 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', '', 'admin', NULL, 0, 0); ``` diff --git a/cloud/migrations/2026-07-17-p1-9-p3-2b.sql b/cloud/migrations/2026-07-17-p1-9-p3-2b.sql index bc59222..3c8c852 100644 --- a/cloud/migrations/2026-07-17-p1-9-p3-2b.sql +++ b/cloud/migrations/2026-07-17-p1-9-p3-2b.sql @@ -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; diff --git a/cloud/migrations/2026-07-20-move-table-prefix.sql b/cloud/migrations/2026-07-20-move-table-prefix.sql new file mode 100644 index 0000000..aec952f --- /dev/null +++ b/cloud/migrations/2026-07-20-move-table-prefix.sql @@ -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; diff --git a/cloud/schema.sql b/cloud/schema.sql index 31931f8..4c03010 100644 --- a/cloud/schema.sql +++ b/cloud/schema.sql @@ -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 = .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). -- =========================================================================== diff --git a/cloud/src/Repository/Pdo/PdoAuditRepository.php b/cloud/src/Repository/Pdo/PdoAuditRepository.php index 25bfb5e..a2d6e95 100644 --- a/cloud/src/Repository/Pdo/PdoAuditRepository.php +++ b/cloud/src/Repository/Pdo/PdoAuditRepository.php @@ -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([ diff --git a/cloud/src/Repository/Pdo/PdoDeviceRepository.php b/cloud/src/Repository/Pdo/PdoDeviceRepository.php index ad0cb25..a67c9aa 100644 --- a/cloud/src/Repository/Pdo/PdoDeviceRepository.php +++ b/cloud/src/Repository/Pdo/PdoDeviceRepository.php @@ -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; diff --git a/cloud/src/Repository/Pdo/PdoEventRepository.php b/cloud/src/Repository/Pdo/PdoEventRepository.php index 8c39dd5..b878705 100644 --- a/cloud/src/Repository/Pdo/PdoEventRepository.php +++ b/cloud/src/Repository/Pdo/PdoEventRepository.php @@ -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; diff --git a/cloud/src/Repository/Pdo/PdoNonceRepository.php b/cloud/src/Repository/Pdo/PdoNonceRepository.php index faaa9ac..718170e 100644 --- a/cloud/src/Repository/Pdo/PdoNonceRepository.php +++ b/cloud/src/Repository/Pdo/PdoNonceRepository.php @@ -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()]); } } diff --git a/cloud/src/Repository/Pdo/PdoRateLimiterRepository.php b/cloud/src/Repository/Pdo/PdoRateLimiterRepository.php index b78db3f..7bbf18f 100644 --- a/cloud/src/Repository/Pdo/PdoRateLimiterRepository.php +++ b/cloud/src/Repository/Pdo/PdoRateLimiterRepository.php @@ -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()); diff --git a/cloud/src/Repository/Pdo/PdoSnapshotRepository.php b/cloud/src/Repository/Pdo/PdoSnapshotRepository.php index 3998654..0ae13c8 100644 --- a/cloud/src/Repository/Pdo/PdoSnapshotRepository.php +++ b/cloud/src/Repository/Pdo/PdoSnapshotRepository.php @@ -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); } diff --git a/cloud/src/Repository/Pdo/PdoThresholdRepository.php b/cloud/src/Repository/Pdo/PdoThresholdRepository.php index 37e041a..779faba 100644 --- a/cloud/src/Repository/Pdo/PdoThresholdRepository.php +++ b/cloud/src/Repository/Pdo/PdoThresholdRepository.php @@ -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 = []; diff --git a/cloud/src/Repository/Pdo/PdoUserRepository.php b/cloud/src/Repository/Pdo/PdoUserRepository.php index dd0dd57..8817285 100644 --- a/cloud/src/Repository/Pdo/PdoUserRepository.php +++ b/cloud/src/Repository/Pdo/PdoUserRepository.php @@ -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]); } } diff --git a/cloud/src/Repository/Pdo/PdoZoneRepository.php b/cloud/src/Repository/Pdo/PdoZoneRepository.php index 9c5285c..05c48b5 100644 --- a/cloud/src/Repository/Pdo/PdoZoneRepository.php +++ b/cloud/src/Repository/Pdo/PdoZoneRepository.php @@ -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'); diff --git a/cloud/tests/PdoIntegrationTest.php b/cloud/tests/PdoIntegrationTest.php index e5e77f0..bcec642 100644 --- a/cloud/tests/PdoIntegrationTest.php +++ b/cloud/tests/PdoIntegrationTest.php @@ -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,