Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions app/Actions/Database/CreateDatabaseUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\Server;
use App\Models\Service;
use App\Services\Database\Database;
use Closure;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
Expand All @@ -27,7 +28,7 @@ public function create(Server $server, array $input, array $links = []): Databas
'server_id' => $server->id,
'username' => $input['username'],
'password' => $input['password'],
'host' => (isset($input['remote']) && $input['remote']) || isset($input['host']) ? $input['host'] : 'localhost',
'host' => $this->resolveHost($input),
'databases' => $links,
'permission' => $input['permission'] ?? 'admin',
]);
Expand Down Expand Up @@ -56,11 +57,19 @@ public function create(Server $server, array $input, array $links = []): Databas

private function validate(Server $server, array $input): void
{
/** @var Database $handler */
$handler = $server->database()->handler();
$host = $this->resolveHost($input);

$rules = [
'username' => [
'required',
'alpha_dash',
Rule::unique('database_users', 'username')->where('server_id', $server->id),
function (string $attribute, mixed $value, Closure $fail) use ($handler, $server, $host): void {
if ($handler->databaseUserExists($server, (string) $value, $host)) {
$fail(__('A database user with this username and host already exists.'));
}
},
],
'password' => [
'required',
Expand All @@ -71,10 +80,26 @@ private function validate(Server $server, array $input): void
Rule::in(['read', 'write', 'admin']),
],
];
if (isset($input['remote']) && $input['remote']) {
$rules['host'] = 'required';

if ($handler->usesHost()) {
$rules['host'] = [
isset($input['remote']) && $input['remote'] ? 'required' : 'nullable',
'regex:/^[A-Za-z0-9%._:\-]*$/',
];
}

Validator::make($input, $rules)->validate();
}

/**
* @param array<string, mixed> $input
*/
private function resolveHost(array $input): string
{
if (! empty($input['host'])) {
return $input['host'];
}

return ! empty($input['remote']) ? '%' : 'localhost';
}
}
50 changes: 40 additions & 10 deletions app/Actions/Database/UpdateDatabaseUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,11 @@ public function update(DatabaseUser $databaseUser, array $input): DatabaseUser
$oldHost = $databaseUser->host;
$oldPermission = $databaseUser->permission;
$newPassword = $input['password'] ?? null;
$newHost = null;
$newHost = $this->changedHost($databaseUser, $input);
$permissionChanged = false;

if (isset($input['remote'])) {
$newHost = $input['remote'] ? ($input['host'] ?? '%') : 'localhost';
if ($newHost !== $oldHost) {
$databaseUser->host = $newHost;
} else {
$newHost = null;
}
if ($newHost !== null) {
$databaseUser->host = $newHost;
}

if ($newPassword) {
Expand Down Expand Up @@ -79,8 +74,14 @@ private function validate(DatabaseUser $databaseUser, array $input): void
];
}

if (isset($input['remote']) && $input['remote']) {
$rules['host'] = 'required';
/** @var Database $handler */
$handler = $databaseUser->server->database()->handler();

if ($handler->usesHost()) {
$rules['host'] = [
isset($input['remote']) && $input['remote'] ? 'required' : 'nullable',
'regex:/^[A-Za-z0-9%._:\-]*$/',
];
}

$rules['permission'] = [
Expand All @@ -89,6 +90,35 @@ private function validate(DatabaseUser $databaseUser, array $input): void
];

Validator::make($input, $rules)->validate();

$newHost = $this->changedHost($databaseUser, $input);
if ($newHost !== null && $handler->databaseUserExists($databaseUser->server, $databaseUser->username, $newHost, $databaseUser)) {
throw ValidationException::withMessages([
'host' => __('A database user with this username and host already exists.'),
]);
}
}

/**
* Resolves the new host when it is being changed, or null when unchanged or
* the database engine does not use hosts (e.g. PostgreSQL).
*
* @param array<string, mixed> $input
*/
private function changedHost(DatabaseUser $databaseUser, array $input): ?string
{
if (! isset($input['remote'])) {
return null;
}

$handler = $databaseUser->server->database()?->handler();
if (! $handler instanceof Database || ! $handler->usesHost()) {
return null;
}

$resolved = $input['remote'] ? (! empty($input['host']) ? $input['host'] : '%') : 'localhost';

return $resolved !== $databaseUser->host ? $resolved : null;
}

private function updatePermissions(DatabaseUser $databaseUser, string $oldHost, ?string $newHost): void
Expand Down
3 changes: 2 additions & 1 deletion app/Enums/DatabaseUserPermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
namespace App\Enums;

use App\Contracts\VitoEnum;
use Forjed\InertiaTable\Contracts\HasTableDisplay;

enum DatabaseUserPermission: string implements VitoEnum
enum DatabaseUserPermission: string implements HasTableDisplay, VitoEnum
{
case READ = 'read';
case WRITE = 'write';
Expand Down
3 changes: 2 additions & 1 deletion app/Enums/DatabaseUserStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
namespace App\Enums;

use App\Contracts\VitoEnum;
use Forjed\InertiaTable\Contracts\HasTableDisplay;

enum DatabaseUserStatus: string implements VitoEnum
enum DatabaseUserStatus: string implements HasTableDisplay, VitoEnum
{
case READY = 'ready';
case CREATING = 'creating';
Expand Down
10 changes: 9 additions & 1 deletion app/Http/Controllers/DatabaseUserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use App\Http\Resources\DatabaseUserResource;
use App\Models\DatabaseUser;
use App\Models\Server;
use App\Services\Database\Database;
use App\Tables\Servers\DatabaseUserTable;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
Expand All @@ -33,9 +35,15 @@ public function index(Server $server): Response
{
$this->authorize('viewAny', [DatabaseUser::class, $server]);

/** @var Database $handler */
$handler = $server->database()->handler();

return Inertia::render('database-users/index', [
'databases' => DatabaseResource::collection($server->databases()->get()),
'databaseUsers' => DatabaseUserResource::collection($server->databaseUsers()->simplePaginate(config('web.pagination_size'))),
'usesHost' => $handler->usesHost(),
'databaseUsers' => DatabaseUserTable::make($server->databaseUsers())
->withHost($handler->usesHost())
->paginate(),
]);
}

Expand Down
3 changes: 2 additions & 1 deletion app/Http/Resources/DatabaseUserResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public function toArray(Request $request): array
'username' => $this->username,
'databases' => $this->databases,
'host' => $this->host,
'permission' => $this->permission,
'permission' => $this->permission->getText(),
'permission_color' => $this->permission->getColor(),
'status' => $this->status->getText(),
'status_color' => $this->status->getColor(),
'created_at' => $this->created_at,
Expand Down
16 changes: 16 additions & 0 deletions app/Services/Database/AbstractDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use App\Exceptions\ServiceInstallationFailed;
use App\Exceptions\SSHError;
use App\Models\BackupFile;
use App\Models\DatabaseUser;
use App\Models\Server;
use App\Services\AbstractService;
use Closure;
use Illuminate\Contracts\View\View;
Expand Down Expand Up @@ -35,6 +37,20 @@ protected function getScriptView(string $script): string
return 'ssh.services.database.'.$this->service->name.'.'.$script;
}

public function usesHost(): bool
{
return true;
}

public function databaseUserExists(Server $server, string $username, string $host, ?DatabaseUser $ignore = null): bool
{
return $server->databaseUsers()
->where('username', $username)
->where('host', $host)
->when($ignore, fn ($query) => $query->whereKeyNot($ignore->id))
->exists();
}

public function creationRules(array $input): array
{
return [
Expand Down
6 changes: 6 additions & 0 deletions app/Services/Database/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
namespace App\Services\Database;

use App\Models\BackupFile;
use App\Models\DatabaseUser;
use App\Models\Server;
use App\Services\ServiceInterface;

interface Database extends ServiceInterface
{
public function usesHost(): bool;

public function databaseUserExists(Server $server, string $username, string $host, ?DatabaseUser $ignore = null): bool;

public function create(string $name, string $charset, string $collation): void;

public function delete(string $name): void;
Expand Down
15 changes: 15 additions & 0 deletions app/Services/Database/Postgresql.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace App\Services\Database;

use App\DTOs\ServiceLog;
use App\Models\DatabaseUser;
use App\Models\Server;
use App\Services\HasLogs;
use Illuminate\Contracts\View\View;

Expand All @@ -23,6 +25,19 @@ class Postgresql extends AbstractDatabase implements HasLogs

protected bool $removeLastRow = true;

public function usesHost(): bool
{
return false;
}

public function databaseUserExists(Server $server, string $username, string $host, ?DatabaseUser $ignore = null): bool
{
return $server->databaseUsers()
->where('username', $username)
->when($ignore, fn ($query) => $query->whereKeyNot($ignore->id))
->exists();
}

public static function id(): string
{
return 'postgresql';
Expand Down
46 changes: 46 additions & 0 deletions app/Tables/Servers/DatabaseUserTable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Tables\Servers;

use Forjed\InertiaTable\Column;
use Forjed\InertiaTable\Columns\ActionsColumn;
use Forjed\InertiaTable\Columns\ComponentColumn;
use Forjed\InertiaTable\Columns\DateTimeColumn;
use Forjed\InertiaTable\Columns\EnumColumn;
use Forjed\InertiaTable\Columns\TextColumn;
use Forjed\InertiaTable\Table;

class DatabaseUserTable extends Table
{
protected bool $showHost = false;

public function withHost(bool $showHost = true): static
{
$this->showHost = $showHost;

return $this;
}

protected function query(): void
{
$this->perPage = config('web.pagination_size');
$this->query->latest();
}

protected function columns(): array
{
return [
TextColumn::make('username', 'Username')->sortable(),
$this->showHost
? TextColumn::make('host', 'Host')->sortable()
: Column::data('host'),
EnumColumn::make('permission', 'Permission'),
ComponentColumn::create('databases', 'Linked databases', 'DatabaseUserDatabases'),
DateTimeColumn::make('created_at', 'Created at')->sortable()->toLocal(),
EnumColumn::make('status', 'Status')->sortable(),
Column::data('id'),
Column::data('server_id'),
ActionsColumn::make(),
];
}
}
20 changes: 20 additions & 0 deletions resources/js/components/database-user-databases.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { CellComponentProps } from '@forjedio/inertia-table-react';
import { Badge } from '@/components/ui/badge';

export function DatabaseUserDatabases({ value }: CellComponentProps) {
const databases = (value as string[] | null) ?? [];

if (databases.length === 0) {
return <span className="text-muted-foreground">-</span>;
}

return (
<div className="flex flex-wrap items-center gap-1">
{databases.map((database) => (
<Badge key={database} variant="outline">
{database}
</Badge>
))}
</div>
);
}
Loading
Loading