317 lines
12 KiB
PHP
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');
|
|
}
|
|
}
|