Files
jabali-panel/app/Livewire/DatabaseUsersTable.php
2026-01-24 19:36:46 +02:00

317 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Livewire;
use App\Models\MysqlCredential;
use App\Services\Agent\AgentClient;
use Filament\Actions\Action;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Forms\Components\CheckboxList;
use Filament\Forms\Components\Radio;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ViewColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Crypt;
use Livewire\Component;
use Exception;
class DatabaseUsersTable extends Component implements HasTable, HasForms, HasActions
{
use InteractsWithTable;
use InteractsWithForms;
use InteractsWithActions;
public array $users = [];
public array $userGrants = [];
public array $databases = [];
public ?string $selectedUser = null;
protected ?AgentClient $agent = null;
protected $listeners = ['refresh-database-users' => 'refreshData'];
public function mount(): void
{
$this->loadData();
}
public function refreshData(): void
{
$this->loadData();
$this->resetTable();
}
public function getAgent(): AgentClient
{
if ($this->agent === null) {
$this->agent = new AgentClient();
}
return $this->agent;
}
public function getUsername(): string
{
return Auth::user()->username;
}
public function loadData(): void
{
try {
$result = $this->getAgent()->mysqlListUsers($this->getUsername());
$this->users = $result['users'] ?? [];
// Filter out the master admin user
$this->users = array_values(array_filter($this->users, function($user) {
return $user['user'] !== $this->getUsername() . '_admin';
}));
$this->userGrants = [];
foreach ($this->users as $user) {
$this->loadUserGrants($user['user'], $user['host']);
}
} catch (Exception $e) {
$this->users = [];
}
try {
$result = $this->getAgent()->mysqlListDatabases($this->getUsername());
$this->databases = $result['databases'] ?? [];
} catch (Exception $e) {
$this->databases = [];
}
}
protected function loadUserGrants(string $user, string $host): void
{
try {
$result = $this->getAgent()->mysqlGetPrivileges($this->getUsername(), $user, $host);
$this->userGrants["$user@$host"] = $result['parsed'] ?? [];
} catch (Exception $e) {
$this->userGrants["$user@$host"] = [];
}
}
public function getUserGrants(string $user, string $host): array
{
return $this->userGrants["$user@$host"] ?? [];
}
public function table(Table $table): Table
{
return $table
->records(fn () => $this->users)
->columns([
TextColumn::make('user')
->label(__('User'))
->icon('heroicon-o-user')
->iconColor('primary')
->description(fn (array $record): string => '@ ' . $record['host'])
->weight('medium')
->searchable(),
ViewColumn::make('privileges')
->label(__('Database Privileges'))
->view('filament.jabali.tables.columns.user-privileges'),
])
->recordActions([
Action::make('addPrivileges')
->label(__('Add Access'))
->icon('heroicon-o-plus')
->color('success')
->modalHeading(__('Add Database Access'))
->modalDescription(fn (array $record): string => __('Grant privileges to :user', ['user' => $record['user'] . '@' . $record['host']]))
->modalIcon('heroicon-o-shield-check')
->modalIconColor('success')
->modalWidth('lg')
->modalSubmitActionLabel(__('Grant Access'))
->form(fn (array $record): array => $this->getPrivilegesForm())
->action(function (array $data, array $record): void {
$this->grantPrivileges($record['user'], $record['host'], $data);
}),
Action::make('changePassword')
->label(__('Password'))
->icon('heroicon-o-key')
->color('warning')
->modalHeading(__('Change Password'))
->modalDescription(fn (array $record): string => $record['user'] . '@' . $record['host'])
->modalIcon('heroicon-o-key')
->modalIconColor('warning')
->modalSubmitActionLabel(__('Change Password'))
->form([
TextInput::make('password')
->label(__('New Password'))
->password()
->revealable()
->required()
->minLength(8)
->default(fn () => $this->generateSecurePassword())
->suffixActions([
Action::make('generate')
->icon('heroicon-o-arrow-path')
->tooltip(__('Generate secure password'))
->action(fn ($set) => $set('password', $this->generateSecurePassword())),
Action::make('copy')
->icon('heroicon-o-clipboard-document')
->tooltip(__('Copy to clipboard'))
->action(function ($state, $livewire) {
if ($state) {
$escaped = addslashes($state);
$livewire->js("navigator.clipboard.writeText('{$escaped}')");
Notification::make()
->title(__('Copied to clipboard'))
->success()
->duration(2000)
->send();
}
}),
]),
])
->action(function (array $data, array $record): void {
$this->changePassword($record['user'], $record['host'], $data['password']);
}),
Action::make('delete')
->label(__('Delete'))
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->modalHeading(__('Delete User'))
->modalDescription(fn (array $record): string => __("Delete user ':user'?", ['user' => $record['user'] . '@' . $record['host']]))
->modalIcon('heroicon-o-trash')
->modalIconColor('danger')
->action(function (array $record): void {
$this->deleteUser($record['user'], $record['host']);
}),
])
->emptyStateHeading(__('No database users yet'))
->emptyStateDescription(__('Click "New User" to create one'))
->emptyStateIcon('heroicon-o-users')
->striped();
}
public function getTableRecordKey(Model|array $record): string
{
return is_array($record) ? $record['user'] . '@' . $record['host'] : $record->getKey();
}
protected function getPrivilegesForm(): array
{
$dbOptions = [];
foreach ($this->databases as $db) {
$dbOptions[$db['name']] = $db['name'];
}
return [
Select::make('database')
->label(__('Database'))
->options($dbOptions)
->required()
->searchable()
->live(),
Radio::make('privilege_type')
->label(__('Privilege Type'))
->options([
'all' => __('ALL PRIVILEGES'),
'specific' => __('Specific privileges'),
])
->default('all')
->required()
->live(),
CheckboxList::make('specific_privileges')
->label(__('Select Privileges'))
->options([
'SELECT' => 'SELECT',
'INSERT' => 'INSERT',
'UPDATE' => 'UPDATE',
'DELETE' => 'DELETE',
'CREATE' => 'CREATE',
'DROP' => 'DROP',
'INDEX' => 'INDEX',
'ALTER' => 'ALTER',
])
->columns(2)
->visible(fn (callable $get): bool => $get('privilege_type') === 'specific'),
];
}
public function grantPrivileges(string $user, string $host, array $data): void
{
$privs = $data['privilege_type'] === 'all' ? ['ALL'] : ($data['specific_privileges'] ?? []);
try {
$this->getAgent()->mysqlGrantPrivileges($this->getUsername(), $user, $data['database'], $privs, $host);
Notification::make()->title(__('Privileges granted'))->success()->send();
$this->loadData();
$this->resetTable();
} catch (Exception $e) {
Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send();
}
}
public function revokePrivileges(string $user, string $host, string $database): void
{
try {
$this->getAgent()->mysqlRevokePrivileges($this->getUsername(), $user, $database, $host);
Notification::make()->title(__('Access revoked'))->success()->send();
$this->loadData();
$this->resetTable();
} catch (Exception $e) {
Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send();
}
}
public function changePassword(string $user, string $host, string $password): void
{
try {
$this->getAgent()->mysqlChangePassword($this->getUsername(), $user, $password, $host);
MysqlCredential::updateOrCreate(
['user_id' => Auth::id(), 'mysql_username' => $user],
['mysql_password_encrypted' => Crypt::encryptString($password)]
);
Notification::make()->title(__('Password changed'))->success()->send();
} catch (Exception $e) {
Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send();
}
}
public function deleteUser(string $user, string $host): void
{
try {
$this->getAgent()->mysqlDeleteUser($this->getUsername(), $user, $host);
MysqlCredential::where('user_id', Auth::id())->where('mysql_username', $user)->delete();
Notification::make()->title(__('User deleted'))->success()->send();
$this->loadData();
$this->resetTable();
} catch (Exception $e) {
Notification::make()->title(__('Error'))->body($e->getMessage())->danger()->send();
}
}
public function generateSecurePassword(int $length = 16): string
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
$password = '';
for ($i = 0; $i < $length; $i++) {
$password .= $chars[random_int(0, strlen($chars) - 1)];
}
return $password;
}
public function render()
{
return view('livewire.database-users-table');
}
}