diff --git a/examples/crash_reporter/backend/README.md b/examples/crash_reporter/backend/README.md index a7bd08d..8684ac5 100644 --- a/examples/crash_reporter/backend/README.md +++ b/examples/crash_reporter/backend/README.md @@ -60,7 +60,14 @@ Config (`config.php`): ``` Permission checks: `Rbac::can('edit_tags', $companyId)` (used by `Auth::canEditTags()`). -Manual SQL: `sql/migrations/001_rbac_companies.sql` (MariaDB). Re-login after deploy so session includes company memberships. +Manual SQL (MariaDB, **as root** — `androidcast` has no `ALTER`): + +```sh +mysql -u root -p androidcast_crashes < sql/migrations/001_rbac_companies.sql +mysql -u root -p androidcast_crashes < sql/migrations/002_reports_company_columns.sql +``` + +Re-login after deploy so session includes company memberships. ## UI languages diff --git a/examples/crash_reporter/backend/scripts/init-mariadb.sh b/examples/crash_reporter/backend/scripts/init-mariadb.sh index 8d557d0..09bf34e 100755 --- a/examples/crash_reporter/backend/scripts/init-mariadb.sh +++ b/examples/crash_reporter/backend/scripts/init-mariadb.sh @@ -7,6 +7,7 @@ set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd)" SCHEMA="$ROOT/sql/schema.mariadb.sql" +MIG002="$ROOT/sql/migrations/002_reports_company_columns.sql" DB_NAME="${CRASH_DB_NAME:-androidcast_crashes}" DB_USER="${CRASH_DB_USER:-androidcast}" DB_PASS="${CRASH_DB_PASS:-}" @@ -19,6 +20,11 @@ fi echo "Applying schema from $SCHEMA ..." mysql -u root -p < "$SCHEMA" +if [ -f "$MIG002" ]; then + echo "Applying RBAC report columns (002) ..." + mysql -u root -p "$DB_NAME" < "$MIG002" +fi + if [ -n "$DB_PASS" ]; then echo "Creating app user ${DB_USER}@localhost and @127.0.0.1 ..." mysql -u root -p <exec($sql); + return true; + } catch (PDOException $e) { + $msg = $label . ': ' . $e->getMessage(); + error_log('crash_reporter schema ' . $msg); + if (function_exists('log_crash_event')) { + log_crash_event('schema', $msg); + } + return false; + } + } + + public static function reportsHaveRbacColumns(?PDO $pdo = null): bool { + $pdo ??= self::$pdo; + if ($pdo === null || !self::tableExists($pdo, 'reports')) { + return false; + } + $names = self::columnNames($pdo, 'reports'); + return in_array('company_id', $names, true); + } + /** Phase 1 RBAC tables + report company/device columns (idempotent). */ public static function ensureRbacSchema(?PDO $pdo = null): void { $pdo ??= self::$pdo; @@ -170,6 +196,12 @@ final class Database { self::ensureDevicesTable($pdo); self::ensureReportCompanyColumns($pdo); self::backfillCompanyMemberships($pdo); + if (!self::reportsHaveRbacColumns($pdo)) { + error_log( + 'crash_reporter: reports.company_id missing — run as MySQL root: ' + . 'mysql -u root -p androidcast_crashes < sql/migrations/002_reports_company_columns.sql' + ); + } } private static function ensureCompaniesTable(PDO $pdo): void { @@ -177,33 +209,45 @@ final class Database { return; } if (self::isMysql()) { - $pdo->exec( + self::execSchemaSql( + $pdo, "CREATE TABLE companies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, slug VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_companies_slug (slug) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + 'create companies' ); return; } - $pdo->exec( + self::execSchemaSql( + $pdo, "CREATE TABLE companies ( id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) - )" + )", + 'create companies' ); } private static function seedDefaultCompany(PDO $pdo): void { if (self::isMysql()) { - $pdo->exec("INSERT IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default')"); + self::execSchemaSql( + $pdo, + "INSERT IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default')", + 'seed company' + ); return; } - $pdo->exec("INSERT OR IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default')"); + self::execSchemaSql( + $pdo, + "INSERT OR IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default')", + 'seed company' + ); } private static function ensureCompanyMembershipsTable(PDO $pdo): void { @@ -211,7 +255,8 @@ final class Database { return; } if (self::isMysql()) { - $pdo->exec( + self::execSchemaSql( + $pdo, "CREATE TABLE company_memberships ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, @@ -221,11 +266,13 @@ final class Database { created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_membership_user_company (user_id, company_id), KEY idx_membership_company (company_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + 'create company_memberships' ); return; } - $pdo->exec( + self::execSchemaSql( + $pdo, "CREATE TABLE company_memberships ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, @@ -234,7 +281,8 @@ final class Database { permissions_json TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE (user_id, company_id) - )" + )", + 'create company_memberships' ); } @@ -243,7 +291,8 @@ final class Database { return; } if (self::isMysql()) { - $pdo->exec( + self::execSchemaSql( + $pdo, "CREATE TABLE devices ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, company_id INT UNSIGNED NOT NULL, @@ -257,11 +306,13 @@ final class Database { meta_json LONGTEXT NULL, UNIQUE KEY uq_devices_company_external (company_id, external_id), KEY idx_devices_company (company_id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci", + 'create devices' ); return; } - $pdo->exec( + self::execSchemaSql( + $pdo, "CREATE TABLE devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_id INTEGER NOT NULL, @@ -274,7 +325,8 @@ final class Database { last_seen_at_ms INTEGER NOT NULL, meta_json TEXT, UNIQUE (company_id, external_id) - )" + )", + 'create devices' ); } @@ -285,22 +337,48 @@ final class Database { $names = self::columnNames($pdo, 'reports'); if (!in_array('company_id', $names, true)) { if (self::isMysql()) { - $pdo->exec( - 'ALTER TABLE reports ADD COLUMN company_id INT UNSIGNED NOT NULL DEFAULT 1' + self::execSchemaSql( + $pdo, + 'ALTER TABLE reports ADD COLUMN company_id INT UNSIGNED NOT NULL DEFAULT 1', + 'alter reports.company_id' + ); + self::execSchemaSql( + $pdo, + 'ALTER TABLE reports ADD KEY idx_reports_company (company_id)', + 'index reports.company_id' ); - $pdo->exec('ALTER TABLE reports ADD KEY idx_reports_company (company_id)'); } else { - $pdo->exec('ALTER TABLE reports ADD COLUMN company_id INTEGER NOT NULL DEFAULT 1'); + self::execSchemaSql( + $pdo, + 'ALTER TABLE reports ADD COLUMN company_id INTEGER NOT NULL DEFAULT 1', + 'alter reports.company_id' + ); } - $pdo->exec('UPDATE reports SET company_id = 1 WHERE company_id IS NULL OR company_id = 0'); + self::execSchemaSql( + $pdo, + 'UPDATE reports SET company_id = 1 WHERE company_id IS NULL OR company_id = 0', + 'backfill reports.company_id' + ); } $names = self::columnNames($pdo, 'reports'); if (!in_array('device_id', $names, true)) { if (self::isMysql()) { - $pdo->exec('ALTER TABLE reports ADD COLUMN device_id BIGINT UNSIGNED NULL'); - $pdo->exec('ALTER TABLE reports ADD KEY idx_reports_device (device_id)'); + self::execSchemaSql( + $pdo, + 'ALTER TABLE reports ADD COLUMN device_id BIGINT UNSIGNED NULL', + 'alter reports.device_id' + ); + self::execSchemaSql( + $pdo, + 'ALTER TABLE reports ADD KEY idx_reports_device (device_id)', + 'index reports.device_id' + ); } else { - $pdo->exec('ALTER TABLE reports ADD COLUMN device_id INTEGER NULL'); + self::execSchemaSql( + $pdo, + 'ALTER TABLE reports ADD COLUMN device_id INTEGER NULL', + 'alter reports.device_id' + ); } } } @@ -340,8 +418,17 @@ final class Database { private static function ensureMysqlReportColumns(PDO $pdo): void { $names = self::columnNames($pdo, 'reports'); if (!in_array('tags_json', $names, true)) { - $pdo->exec("ALTER TABLE reports ADD COLUMN tags_json LONGTEXT NOT NULL"); - $pdo->exec("UPDATE reports SET tags_json = '[]' WHERE tags_json IS NULL OR tags_json = ''"); + if (self::execSchemaSql( + $pdo, + 'ALTER TABLE reports ADD COLUMN tags_json LONGTEXT NOT NULL', + 'alter reports.tags_json' + )) { + self::execSchemaSql( + $pdo, + "UPDATE reports SET tags_json = '[]' WHERE tags_json IS NULL OR tags_json = ''", + 'backfill reports.tags_json' + ); + } } } @@ -364,7 +451,11 @@ final class Database { private static function ensureReportColumns(PDO $pdo): void { $names = self::columnNames($pdo, 'reports'); if (!in_array('tags_json', $names, true)) { - $pdo->exec("ALTER TABLE reports ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]'"); + self::execSchemaSql( + $pdo, + "ALTER TABLE reports ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]'", + 'alter reports.tags_json' + ); } }