Update: 2026-07-30 02:27:45
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* CLI-ONLY migration script.
|
||||
* Moved out of the public webroot on 2026-07-30 - it used to be reachable
|
||||
* unauthenticated over HTTP. Run with: php scripts/legacy_migrations/<file>
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit('Not Found');
|
||||
}
|
||||
require_once __DIR__ . '/../../app/bootstrap/init.php';
|
||||
use App\Core\Database;
|
||||
use App\Core\Encryption;
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$db->beginTransaction();
|
||||
|
||||
// 1. Generate UUIDs
|
||||
$tenantId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
);
|
||||
|
||||
$userId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
|
||||
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
|
||||
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
|
||||
);
|
||||
|
||||
// 2. Test Account Data
|
||||
$tenantName = "مكتب المراجعة التجريبي";
|
||||
$tenantEmail = "reviewer@musadaq.jo";
|
||||
|
||||
$userName = "App Reviewer";
|
||||
$userEmail = "reviewer@musadaq.jo";
|
||||
// Password must be supplied on the command line - never hardcoded.
|
||||
// php scripts/legacy_migrations/create_test_account.php '<password>'
|
||||
$userPassword = $argv[1] ?? '';
|
||||
if (strlen($userPassword) < 12) {
|
||||
exit("Usage: php create_test_account.php '<password>' (min 12 chars)\n");
|
||||
}
|
||||
|
||||
// 3. Encrypt data
|
||||
$encryptedTenantName = Encryption::encrypt($tenantName);
|
||||
$encryptedTenantEmail = Encryption::encrypt($tenantEmail);
|
||||
|
||||
$encryptedUserName = Encryption::encrypt($userName);
|
||||
$encryptedUserEmail = Encryption::encrypt($userEmail);
|
||||
$emailHash = hash('sha256', strtolower($userEmail));
|
||||
$passwordHash = password_hash($userPassword, PASSWORD_DEFAULT);
|
||||
|
||||
// 4. Delete existing if any (prevent duplicates on re-run)
|
||||
$stmt = $db->prepare("DELETE FROM users WHERE email_hash = ?");
|
||||
$stmt->execute([$emailHash]);
|
||||
|
||||
// 5. Insert Tenant
|
||||
$stmt = $db->prepare("INSERT INTO tenants (id, name, email, status, created_at) VALUES (?, ?, ?, 'active', NOW())");
|
||||
$stmt->execute([
|
||||
$tenantId,
|
||||
$encryptedTenantName,
|
||||
$encryptedTenantEmail
|
||||
]);
|
||||
|
||||
// 6. Insert User (Manager)
|
||||
$stmtUser = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, created_at) VALUES (?, ?, ?, ?, ?, ?, 'admin', NOW())");
|
||||
$stmtUser->execute([
|
||||
$userId,
|
||||
$tenantId,
|
||||
$encryptedUserName,
|
||||
$encryptedUserEmail,
|
||||
$emailHash,
|
||||
$passwordHash
|
||||
]);
|
||||
|
||||
$db->commit();
|
||||
echo "<h3 style='color:green'>✅ تم إنشاء الحساب التجريبي بنجاح!</h3>";
|
||||
echo "<p><b>البريد الإلكتروني:</b> $userEmail</p>";
|
||||
echo "<p><b>كلمة المرور:</b> (the one you passed on the command line)</p>";
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$db->rollBack();
|
||||
echo "<h3 style='color:red'>❌ Error: " . htmlspecialchars($e->getMessage()) . "</h3>";
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* CLI-ONLY migration script.
|
||||
* Moved out of the public webroot on 2026-07-30 - it used to be reachable
|
||||
* unauthenticated over HTTP. Run with: php scripts/legacy_migrations/<file>
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit('Not Found');
|
||||
}
|
||||
require_once __DIR__ . '/../../app/bootstrap/init.php';
|
||||
use App\Core\Database;
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = file_get_contents(__DIR__ . '/../../database/migrations/008_invoice_lines_enhance.sql');
|
||||
|
||||
// Split by semicolon and execute
|
||||
$queries = array_filter(array_map('trim', explode(';', $sql)));
|
||||
|
||||
echo "<h1>Running Migration 008...</h1>";
|
||||
echo "<ul>";
|
||||
foreach ($queries as $query) {
|
||||
if (empty($query)) continue;
|
||||
try {
|
||||
$db->exec($query);
|
||||
echo "<li>✅ Executed: <pre>" . htmlspecialchars(substr($query, 0, 70)) . "...</pre></li>";
|
||||
} catch (\Exception $innerE) {
|
||||
if (str_contains($innerE->getMessage(), 'Duplicate column name')) {
|
||||
echo "<li>ℹ️ تخطي (العمود موجود مسبقاً): <pre>" . htmlspecialchars(substr($query, 0, 70)) . "...</pre></li>";
|
||||
} else {
|
||||
throw $innerE;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "</ul>";
|
||||
echo "<h2>Migration completed successfully!</h2>";
|
||||
echo "<p>Please delete this file (public/migrate_008.php) for security.</p>";
|
||||
|
||||
} catch (\Exception $e) {
|
||||
echo "<h2 style='color:red;'>Migration failed:</h2>";
|
||||
echo "<pre>" . htmlspecialchars($e->getMessage()) . "</pre>";
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* CLI-ONLY migration script.
|
||||
* Moved out of the public webroot on 2026-07-30 - it used to be reachable
|
||||
* unauthenticated over HTTP. Run with: php scripts/legacy_migrations/<file>
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit('Not Found');
|
||||
}
|
||||
require_once __DIR__ . '/../../app/bootstrap/init.php';
|
||||
use App\Core\Database;
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "Checking columns in 'users' table...\n";
|
||||
$stmt = $db->query("DESCRIBE users");
|
||||
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
|
||||
if (!in_array('phone_hash', $columns)) {
|
||||
echo "Adding 'phone_hash' column...\n";
|
||||
$db->exec("ALTER TABLE users ADD COLUMN phone_hash VARCHAR(64) NULL AFTER phone");
|
||||
$db->exec("CREATE INDEX idx_phone_hash ON users(phone_hash)");
|
||||
echo "Column 'phone_hash' added successfully.\n";
|
||||
} else {
|
||||
echo "Column 'phone_hash' already exists.\n";
|
||||
}
|
||||
|
||||
if (!in_array('email_hash', $columns)) {
|
||||
echo "Adding 'email_hash' column...\n";
|
||||
$db->exec("ALTER TABLE users ADD COLUMN email_hash VARCHAR(64) NULL AFTER email");
|
||||
$db->exec("CREATE INDEX idx_email_hash ON users(email_hash)");
|
||||
echo "Column 'email_hash' added successfully.\n";
|
||||
} else {
|
||||
echo "Column 'email_hash' already exists.\n";
|
||||
}
|
||||
|
||||
echo "Migration completed.\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* CLI-ONLY migration script.
|
||||
* Moved out of the public webroot on 2026-07-30 - it used to be reachable
|
||||
* unauthenticated over HTTP. Run with: php scripts/legacy_migrations/<file>
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit('Not Found');
|
||||
}
|
||||
require_once __DIR__ . '/../../app/bootstrap/init.php';
|
||||
use App\Core\Database;
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
echo "Running Phase 2 Migrations...\n";
|
||||
|
||||
// 1. user_devices
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS user_devices (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
user_id CHAR(36) NOT NULL,
|
||||
device_fingerprint VARCHAR(64) NOT NULL,
|
||||
device_name VARCHAR(100),
|
||||
platform ENUM('android','ios') NOT NULL,
|
||||
app_version VARCHAR(20),
|
||||
push_token TEXT NULL,
|
||||
device_secret VARCHAR(255) NULL,
|
||||
is_trusted BOOLEAN DEFAULT FALSE,
|
||||
last_seen_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
UNIQUE KEY uq_user_device (user_id, device_fingerprint)
|
||||
)
|
||||
");
|
||||
echo "Created user_devices table.\n";
|
||||
|
||||
// 2. invoice_batches
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS invoice_batches (
|
||||
id CHAR(36) PRIMARY KEY,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
company_id CHAR(36) NOT NULL,
|
||||
uploaded_by CHAR(36) NOT NULL,
|
||||
total_images INT NOT NULL DEFAULT 0,
|
||||
processed_images INT NOT NULL DEFAULT 0,
|
||||
status ENUM('uploading','processing','done','partial_fail') DEFAULT 'uploading',
|
||||
source ENUM('mobile_scan','web_upload','whatsapp') DEFAULT 'mobile_scan',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at DATETIME NULL,
|
||||
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
");
|
||||
echo "Created invoice_batches table.\n";
|
||||
|
||||
// 3. invoice_processing_queue
|
||||
$db->exec("
|
||||
CREATE TABLE IF NOT EXISTS invoice_processing_queue (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
batch_id CHAR(36) NOT NULL,
|
||||
invoice_id CHAR(36) NULL,
|
||||
tenant_id CHAR(36) NOT NULL,
|
||||
image_path VARCHAR(500) NOT NULL,
|
||||
status ENUM('pending','processing','done','failed') DEFAULT 'pending',
|
||||
attempts INT DEFAULT 0,
|
||||
error_message TEXT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at DATETIME NULL,
|
||||
FOREIGN KEY (batch_id) REFERENCES invoice_batches(id) ON DELETE CASCADE
|
||||
)
|
||||
");
|
||||
// MySQL has no "CREATE INDEX IF NOT EXISTS" - check information_schema instead.
|
||||
$idxStmt = $db->prepare("
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND table_name = 'invoice_processing_queue'
|
||||
AND index_name = 'idx_status_tenant'
|
||||
");
|
||||
$idxStmt->execute();
|
||||
if ((int)$idxStmt->fetchColumn() === 0) {
|
||||
$db->exec("CREATE INDEX idx_status_tenant ON invoice_processing_queue (status, tenant_id)");
|
||||
}
|
||||
echo "Created invoice_processing_queue table.\n";
|
||||
|
||||
echo "Phase 2 Migrations completed successfully.\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
/**
|
||||
* Migration: Queue Hardening (2026-07-30)
|
||||
*
|
||||
* Brings the live schema in line with the fixed queue/processing code:
|
||||
* - invoice_processing_queue.claimed_at -> detects workers that died mid-item
|
||||
* - invoice_processing_queue.max_attempts -> retry ceiling (was referenced, never existed on old installs)
|
||||
* - invoice_processing_queue.image_order -> per-image ordering
|
||||
* - invoice_processing_queue.company_id -> written by batches/upload_image.php
|
||||
* - invoice_batches.failed_images -> failure accounting for completion
|
||||
* - invoice_batches.updated_at -> touched on every upload
|
||||
* - invoice_batches.status -> adds 'failed' to the enum
|
||||
* - invoices.batch_id -> links an invoice back to its batch
|
||||
* - ai_usage_log.tenant_id -> per-office AI cost attribution
|
||||
*
|
||||
* Idempotent: safe to run repeatedly.
|
||||
*
|
||||
* Usage: php scripts/migrate_queue_hardening.php
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit('Not Found');
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../app/bootstrap/init.php';
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
function columnExists(\PDO $db, string $table, string $column): bool
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?
|
||||
");
|
||||
$stmt->execute([$table, $column]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function tableExists(\PDO $db, string $table): bool
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = ?
|
||||
");
|
||||
$stmt->execute([$table]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function indexExists(\PDO $db, string $table, string $index): bool
|
||||
{
|
||||
$stmt = $db->prepare("
|
||||
SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?
|
||||
");
|
||||
$stmt->execute([$table, $index]);
|
||||
return (int)$stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function addColumn(\PDO $db, string $table, string $column, string $definition): void
|
||||
{
|
||||
if (!tableExists($db, $table)) {
|
||||
echo " - skip {$table}.{$column} (table missing)\n";
|
||||
return;
|
||||
}
|
||||
if (columnExists($db, $table, $column)) {
|
||||
echo " = {$table}.{$column} already present\n";
|
||||
return;
|
||||
}
|
||||
$db->exec("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}");
|
||||
echo " + {$table}.{$column} added\n";
|
||||
}
|
||||
|
||||
function addIndex(\PDO $db, string $table, string $index, string $columns): void
|
||||
{
|
||||
if (!tableExists($db, $table)) return;
|
||||
if (indexExists($db, $table, $index)) {
|
||||
echo " = index {$table}.{$index} already present\n";
|
||||
return;
|
||||
}
|
||||
$db->exec("CREATE INDEX `{$index}` ON `{$table}` ({$columns})");
|
||||
echo " + index {$table}.{$index} added\n";
|
||||
}
|
||||
|
||||
echo "=== Queue hardening migration ===\n";
|
||||
|
||||
try {
|
||||
echo "\n[1] invoice_processing_queue\n";
|
||||
addColumn($db, 'invoice_processing_queue', 'company_id', "CHAR(36) NULL AFTER tenant_id");
|
||||
addColumn($db, 'invoice_processing_queue', 'image_order', "INT NOT NULL DEFAULT 0 AFTER image_path");
|
||||
addColumn($db, 'invoice_processing_queue', 'attempts', "INT NOT NULL DEFAULT 0");
|
||||
addColumn($db, 'invoice_processing_queue', 'max_attempts', "INT NOT NULL DEFAULT 3 AFTER attempts");
|
||||
addColumn($db, 'invoice_processing_queue', 'claimed_at', "DATETIME NULL AFTER created_at");
|
||||
addIndex($db, 'invoice_processing_queue', 'idx_batch', 'batch_id');
|
||||
addIndex($db, 'invoice_processing_queue', 'idx_claim', 'status, attempts');
|
||||
|
||||
echo "\n[2] invoice_batches\n";
|
||||
addColumn($db, 'invoice_batches', 'failed_images', "INT NOT NULL DEFAULT 0 AFTER processed_images");
|
||||
addColumn($db, 'invoice_batches', 'updated_at', "DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP");
|
||||
if (tableExists($db, 'invoice_batches')) {
|
||||
// Widen the status enum so a fully-failed batch has a terminal state.
|
||||
$db->exec("
|
||||
ALTER TABLE invoice_batches
|
||||
MODIFY COLUMN status ENUM('uploading','processing','done','partial_fail','failed')
|
||||
NOT NULL DEFAULT 'uploading'
|
||||
");
|
||||
echo " ~ invoice_batches.status enum widened (adds 'failed')\n";
|
||||
}
|
||||
|
||||
echo "\n[3] invoices\n";
|
||||
addColumn($db, 'invoices', 'batch_id', "CHAR(36) NULL AFTER company_id");
|
||||
addIndex($db, 'invoices', 'idx_batch_id', 'batch_id');
|
||||
|
||||
echo "\n[4] ai_usage_log\n";
|
||||
addColumn($db, 'ai_usage_log', 'tenant_id', "CHAR(36) NULL AFTER id");
|
||||
addIndex($db, 'ai_usage_log', 'idx_tenant', 'tenant_id');
|
||||
|
||||
echo "\n[5] user_devices (Live Activity + per-device refresh tokens)\n";
|
||||
addColumn($db, 'user_devices', 'live_activity_token', "TEXT NULL AFTER push_token");
|
||||
addColumn($db, 'user_devices', 'refresh_token_hash', "CHAR(64) NULL AFTER device_secret");
|
||||
addColumn($db, 'user_devices', 'refresh_expires_at', "DATETIME NULL AFTER refresh_token_hash");
|
||||
addIndex($db, 'user_devices', 'idx_refresh_token', 'refresh_token_hash');
|
||||
|
||||
echo "\n[6] backfill invoices.batch_id from the queue\n";
|
||||
if (tableExists($db, 'invoices') && tableExists($db, 'invoice_processing_queue')) {
|
||||
$backfilled = $db->exec("
|
||||
UPDATE invoices i
|
||||
JOIN invoice_processing_queue q ON q.invoice_id = i.id
|
||||
SET i.batch_id = q.batch_id
|
||||
WHERE i.batch_id IS NULL AND q.batch_id IS NOT NULL
|
||||
");
|
||||
echo " ~ backfilled {$backfilled} invoice(s)\n";
|
||||
}
|
||||
|
||||
echo "\n[7] release rows stuck in 'processing' from the pre-fix code\n";
|
||||
if (tableExists($db, 'invoice_processing_queue')) {
|
||||
$released = $db->exec("
|
||||
UPDATE invoice_processing_queue
|
||||
SET status = 'pending', claimed_at = NULL, attempts = 0,
|
||||
error_message = 'Released by queue-hardening migration'
|
||||
WHERE status = 'processing'
|
||||
");
|
||||
echo " ~ released {$released} row(s)\n";
|
||||
}
|
||||
|
||||
echo "\n[8] recount failed_images from the queue (was never populated)\n";
|
||||
if (tableExists($db, 'invoice_batches')) {
|
||||
$db->exec("
|
||||
UPDATE invoice_batches b
|
||||
SET b.failed_images = (
|
||||
SELECT COUNT(*) FROM invoice_processing_queue q
|
||||
WHERE q.batch_id = b.id AND q.status = 'failed'
|
||||
)
|
||||
");
|
||||
echo " ~ failed_images recounted\n";
|
||||
|
||||
$db->exec("
|
||||
UPDATE invoice_batches b
|
||||
SET b.processed_images = (
|
||||
SELECT COUNT(*) FROM invoice_processing_queue q
|
||||
WHERE q.batch_id = b.id AND q.status = 'done'
|
||||
)
|
||||
");
|
||||
echo " ~ processed_images recounted\n";
|
||||
}
|
||||
|
||||
echo "\n=== Migration completed successfully ===\n";
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
echo "\n!!! MIGRATION FAILED: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user