php-developer/src/View/Modal/Admin/Database.php
Snider 518305142e refactor: relocate module from app/Mod/Developer to src/
Move module to standard package structure with namespace change
from Mod\Developer to Core\Developer. Updates composer.json
autoload configuration accordingly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:25:31 +00:00

153 lines
4.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Core\Developer\View\Modal\Admin;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Database Query')]
#[Layout('hub::admin.layouts.app')]
class Database extends Component
{
public string $query = '';
public array $results = [];
public array $columns = [];
public string $error = '';
public bool $processing = false;
public int $rowCount = 0;
public float $executionTime = 0;
public int $maxRows = 500;
protected const MAX_ROWS = 500;
protected const ALLOWED_STATEMENTS = ['SELECT', 'SHOW', 'DESCRIBE', 'EXPLAIN'];
public function mount(): void
{
$this->checkHadesAccess();
}
public function executeQuery(): void
{
$this->reset(['results', 'columns', 'error', 'rowCount', 'executionTime']);
$this->processing = true;
$normalised = $this->normaliseQuery($this->query);
if (empty($normalised)) {
$this->error = 'Please enter a SQL query.';
$this->processing = false;
return;
}
if (! $this->isReadOnlyQuery($normalised)) {
$this->error = 'Only read-only queries are allowed (SELECT, SHOW, DESCRIBE, EXPLAIN).';
$this->processing = false;
Log::warning('Database query tool: blocked non-read-only query', [
'user_id' => auth()->id(),
'user_email' => auth()->user()?->email,
'query' => $this->query,
'ip' => request()->ip(),
]);
return;
}
try {
$startTime = microtime(true);
$results = DB::select($normalised);
$this->executionTime = round((microtime(true) - $startTime) * 1000, 2);
// Convert to array and limit results
$this->results = array_slice(
array_map(fn ($row) => (array) $row, $results),
0,
self::MAX_ROWS
);
$this->rowCount = count($results);
// Extract column names from first result
if (! empty($this->results)) {
$this->columns = array_keys($this->results[0]);
}
Log::info('Database query executed', [
'user_id' => auth()->id(),
'user_email' => auth()->user()?->email,
'query' => $normalised,
'row_count' => $this->rowCount,
'execution_time_ms' => $this->executionTime,
'ip' => request()->ip(),
]);
} catch (\Exception $e) {
$this->error = $e->getMessage();
Log::warning('Database query failed', [
'user_id' => auth()->id(),
'user_email' => auth()->user()?->email,
'query' => $normalised,
'error' => $e->getMessage(),
'ip' => request()->ip(),
]);
}
$this->processing = false;
}
public function clearQuery(): void
{
$this->reset(['query', 'results', 'columns', 'error', 'rowCount', 'executionTime']);
}
public function getConnectionInfoProperty(): array
{
$connection = DB::connection();
return [
'database' => $connection->getDatabaseName(),
'driver' => $connection->getDriverName(),
];
}
protected function normaliseQuery(string $query): string
{
// Trim whitespace and normalise
return trim(preg_replace('/\s+/', ' ', $query));
}
protected function isReadOnlyQuery(string $query): bool
{
// Get first word of query
$firstWord = strtoupper(strtok($query, ' '));
return in_array($firstWord, self::ALLOWED_STATEMENTS, true);
}
private function checkHadesAccess(): void
{
if (! auth()->user()?->isHades()) {
abort(403, 'Hades access required');
}
}
public function render(): View
{
return view('developer::admin.database');
}
}