getConnection(); echo "šŸ”’ Starting API Keys Hardening migration...\n\n"; $tableName = 'api_keys'; $columns = [ 'rate_limit_per_minute' => "INT DEFAULT 100 COMMENT 'Rate limit per minute (default: 100)'", 'rate_limit_window' => "INT DEFAULT 60 COMMENT 'Rate limit window in seconds (default: 60)'", 'enable_ip_whitelist' => "TINYINT(1) DEFAULT 0 COMMENT 'Enable IP whitelist (0=disabled, 1=enabled)'", 'ip_whitelist' => "TEXT NULL COMMENT 'IP whitelist (comma-separated or JSON array)'", 'expires_at' => "DATETIME NULL COMMENT 'API key expiration date (NULL = never expires)'", 'last_used_at' => "DATETIME NULL COMMENT 'Last time API key was used'", 'created_at' => "DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'API key creation date'", 'updated_at' => "DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Last update date'" ]; $indexes = [ 'idx_api_keys_expires_at' => 'expires_at', 'idx_api_keys_is_active' => 'is_active', 'idx_api_keys_last_used_at' => 'last_used_at' ]; $successCount = 0; $errorCount = 0; // Add columns foreach ($columns as $columnName => $columnDef) { try { // Check if column exists $checkSql = "SELECT COUNT(*) as cnt FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table AND COLUMN_NAME = :column"; $result = $db->fetchOne($checkSql, [ 'table' => $tableName, 'column' => $columnName ]); if ($result && $result->cnt == 0) { // Column doesn't exist, add it // Remove COMMENT from column definition for ALTER TABLE $cleanDef = preg_replace('/\s+COMMENT\s+[\'"][^\'"]*[\'"]/i', '', $columnDef); $addSql = "ALTER TABLE `{$tableName}` ADD COLUMN `{$columnName}` {$cleanDef}"; $connection->exec($addSql); echo "āœ… Added column: {$tableName}.{$columnName}\n"; $successCount++; } else { echo "ā­ļø Column already exists: {$tableName}.{$columnName}\n"; } } catch (\PDOException $e) { if (strpos($e->getMessage(), 'Duplicate column') !== false) { echo "ā­ļø Column already exists: {$tableName}.{$columnName}\n"; } else { echo "āŒ Error adding column {$columnName}: " . $e->getMessage() . "\n"; $errorCount++; } } } // Create indexes foreach ($indexes as $indexName => $columnName) { try { // Check if index exists $checkSql = "SELECT COUNT(*) as cnt FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table AND INDEX_NAME = :index"; $result = $db->fetchOne($checkSql, [ 'table' => $tableName, 'index' => $indexName ]); if ($result && $result->cnt == 0) { // Index doesn't exist, create it $createSql = "CREATE INDEX `{$indexName}` ON `{$tableName}` (`{$columnName}`)"; $connection->exec($createSql); echo "āœ… Created index: {$indexName} on {$tableName}({$columnName})\n"; $successCount++; } else { echo "ā­ļø Index already exists: {$indexName}\n"; } } catch (\PDOException $e) { if (strpos($e->getMessage(), 'Duplicate key name') !== false || strpos($e->getMessage(), 'already exists') !== false) { echo "ā­ļø Index already exists: {$indexName}\n"; } else { echo "āŒ Error creating index {$indexName}: " . $e->getMessage() . "\n"; $errorCount++; } } } echo "\nšŸ“Š Migration Summary:\n"; echo " āœ… Success: {$successCount}\n"; echo " āŒ Errors: {$errorCount}\n"; if ($errorCount == 0) { echo "\nšŸŽ‰ Hardening migration completed successfully!\n"; echo "\nšŸ“‹ Hardening Features Enabled:\n"; echo " āœ… Rate Limiting (default: 100 req/min)\n"; echo " āœ… IP Whitelist (optional, per API key)\n"; echo " āœ… API Key Expiration (optional, per API key)\n"; echo " āœ… Request Timestamp Validation (optional)\n"; exit(0); } else { echo "\nāš ļø Migration completed with errors. Please review above.\n"; exit(1); } } catch (\Exception $e) { echo "āŒ Migration failed: " . $e->getMessage() . "\n"; exit(1); }