Merge remote-tracking branch 'upstream/main' into boy132/multiple-startup-commands

This commit is contained in:
Boy132 2025-09-08 10:03:25 +02:00
commit d17d753c63
33 changed files with 237 additions and 177 deletions

View File

@ -3,11 +3,11 @@
namespace App\Console\Commands\Server;
use App\Models\Server;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Factory as ValidatorFactory;
use App\Repositories\Daemon\DaemonPowerRepository;
use Exception;
class BulkPowerActionCommand extends Command
@ -19,7 +19,7 @@ class BulkPowerActionCommand extends Command
protected $description = 'Perform bulk power management on large groupings of servers or nodes at once.';
public function handle(DaemonPowerRepository $powerRepository, ValidatorFactory $validator): void
public function handle(DaemonServerRepository $serverRepository, ValidatorFactory $validator): void
{
$action = $this->argument('action');
$nodes = empty($this->option('nodes')) ? [] : explode(',', $this->option('nodes'));
@ -52,7 +52,7 @@ class BulkPowerActionCommand extends Command
$bar = $this->output->createProgressBar($count);
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $powerRepository, &$bar): mixed {
$this->getQueryBuilder($servers, $nodes)->get()->each(function ($server, int $index) use ($action, $serverRepository, &$bar): mixed {
$bar->clear();
if (!$server instanceof Server) {
@ -60,7 +60,7 @@ class BulkPowerActionCommand extends Command
}
try {
$powerRepository->setServer($server)->send($action);
$serverRepository->setServer($server)->power($action);
} catch (Exception $exception) {
$this->output->error(trans('command/messages.server.power.action_failed', [
'name' => $server->name,

View File

@ -7,7 +7,7 @@ use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Models\ServerVariable;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Repositories\Daemon\DaemonServerRepository;
use Closure;
use Exception;
use Filament\Actions\Action;
@ -76,7 +76,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
->prefix(fn () => '{{' . $serverVariable->variable->env_variable . '}}')
->helperText(fn () => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description),
])
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server, $serverVariable) {
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server, $serverVariable) {
/** @var Server $server */
$server = Filament::getTenant();
try {
@ -98,7 +98,7 @@ class GSLTokenSchema implements FeatureSchemaInterface
->log();
}
$powerRepository->setServer($server)->send('restart');
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('GSL Token updated')

View File

@ -6,7 +6,7 @@ use App\Extensions\Features\FeatureSchemaInterface;
use App\Facades\Activity;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
@ -59,7 +59,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
->preload()
->native(false),
])
->action(function (array $data, DaemonPowerRepository $powerRepository) use ($server) {
->action(function (array $data, DaemonServerRepository $serverRepository) use ($server) {
try {
$new = $data['image'];
$original = $server->image;
@ -71,7 +71,7 @@ class JavaVersionSchema implements FeatureSchemaInterface
->log();
}
$powerRepository->setServer($server)->send('restart');
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('Docker image updated')

View File

@ -5,7 +5,7 @@ namespace App\Extensions\Features\Schemas;
use App\Extensions\Features\FeatureSchemaInterface;
use App\Models\Server;
use App\Repositories\Daemon\DaemonFileRepository;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Repositories\Daemon\DaemonServerRepository;
use Exception;
use Filament\Actions\Action;
use Filament\Facades\Filament;
@ -35,14 +35,14 @@ class MinecraftEulaSchema implements FeatureSchemaInterface
->modalHeading('Minecraft EULA')
->modalDescription(new HtmlString(Blade::render('By pressing "I Accept" below you are indicating your agreement to the <x-filament::link href="https://minecraft.net/eula" target="_blank">Minecraft EULA </x-filament::link>.')))
->modalSubmitActionLabel('I Accept')
->action(function (DaemonFileRepository $fileRepository, DaemonPowerRepository $powerRepository) {
->action(function (DaemonFileRepository $fileRepository, DaemonServerRepository $serverRepository) {
try {
/** @var Server $server */
$server = Filament::getTenant();
$fileRepository->setServer($server)->putContent('eula.txt', 'eula=true');
$powerRepository->setServer($server)->send('restart');
$serverRepository->setServer($server)->power('restart');
Notification::make()
->title('Minecraft EULA accepted')

View File

@ -10,6 +10,7 @@ use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Resources\Pages\CreateRecord;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
class CreateApiKey extends CreateRecord
{
@ -36,7 +37,7 @@ class CreateApiKey extends CreateRecord
protected function handleRecordCreation(array $data): Model
{
$data['identifier'] = ApiKey::generateTokenIdentifier(ApiKey::TYPE_APPLICATION);
$data['token'] = str_random(ApiKey::KEY_LENGTH);
$data['token'] = Str::random(ApiKey::KEY_LENGTH);
$data['user_id'] = auth()->user()->id;
$data['key_type'] = ApiKey::TYPE_APPLICATION;

View File

@ -709,8 +709,22 @@ class EditServer extends EditRecord
->modalHeading(trans('admin/server.delete_db_heading'))
->modalSubmitActionLabel(trans('filament-actions::delete.single.label'))
->modalDescription(fn (Get $get) => trans('admin/server.delete_db', ['name' => $get('database')]))
->action(function (DatabaseManagementService $databaseManagementService, $record) {
$databaseManagementService->delete($record);
->action(function (DatabaseManagementService $service, $record) {
try {
$service->delete($record);
Notification::make()
->title(trans('server/database.delete_notification', ['database' => $record->database]))
->success()
->send();
} catch (Exception $e) {
Notification::make()
->title(trans('server/database.delete_notification_fail', ['database' => $record->database]))
->body($e->getMessage())
->danger()
->persistent()->send();
}
$this->fillForm();
})
),
@ -759,21 +773,22 @@ class EditServer extends EditRecord
->label(fn () => DatabaseHost::query()->count() < 1 ? trans('admin/server.no_db_hosts') : trans('admin/server.create_database'))
->color(fn () => DatabaseHost::query()->count() < 1 ? 'danger' : 'primary')
->modalSubmitActionLabel(trans('admin/server.create_database'))
->action(function (array $data, DatabaseManagementService $service, Server $server, RandomWordService $randomWordService) {
if (empty($data['database'])) {
$data['database'] = $randomWordService->word() . random_int(1, 420);
}
if (empty($data['remote'])) {
$data['remote'] = '%';
}
->action(function (array $data, DatabaseManagementService $service, Server $server) {
$data['database'] ??= str_random(12);
$data['remote'] ??= '%';
$data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id);
try {
$service->setValidateDatabaseLimit(false)->create($server, $data);
Notification::make()
->title(trans('server/database.create_notification', ['database' => $data['database']]))
->success()
->send();
} catch (Exception $e) {
Notification::make()
->title(trans('admin/server.failed_to_create'))
->title(trans('server/database.create_notification_fail', ['database' => $data['database']]))
->body($e->getMessage())
->danger()
->persistent()->send();

View File

@ -8,7 +8,7 @@ use App\Filament\Components\Tables\Columns\ServerEntryColumn;
use App\Filament\Server\Pages\Console;
use App\Models\Permission;
use App\Models\Server;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Filament\Notifications\Notification;
@ -38,11 +38,11 @@ class ListServers extends ListRecords
public const WARNING_THRESHOLD = 0.7;
private DaemonPowerRepository $daemonPowerRepository;
private DaemonServerRepository $daemonServerRepository;
public function boot(): void
{
$this->daemonPowerRepository = new DaemonPowerRepository();
$this->daemonServerRepository = new DaemonServerRepository();
}
/** @return Stack[] */
@ -204,7 +204,7 @@ class ListServers extends ListRecords
public function powerAction(Server $server, string $action): void
{
try {
$this->daemonPowerRepository->setServer($server)->send($action);
$this->daemonServerRepository->setServer($server)->power($action);
Notification::make()
->title(trans('server/dashboard.power_actions'))

View File

@ -4,9 +4,10 @@ namespace App\Filament\Components\Forms\Actions;
use App\Facades\Activity;
use App\Models\Database;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Databases\DatabaseManagementService;
use Exception;
use Filament\Actions\StaticAction;
use Filament\Facades\Filament;
use Filament\Forms\Components\Actions\Action;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
@ -36,10 +37,9 @@ class RotateDatabasePasswordAction extends Action
$this->requiresConfirmation();
$this->action(function (DatabasePasswordService $service, Database $database, Set $set) {
$this->action(function (DatabaseManagementService $service, Database $database, Set $set) {
try {
$service->handle($database);
$service->rotatePassword($database);
$database->refresh();
$set('password', $database->password);
@ -57,7 +57,7 @@ class RotateDatabasePasswordAction extends Action
} catch (Exception $exception) {
Notification::make()
->title(trans('admin/databasehost.rotate_error'))
->body($exception->getMessage())
->body(fn () => auth()->user()->canAccessPanel(Filament::getPanel('admin')) ? $exception->getMessage() : null)
->danger()
->send();

View File

@ -15,9 +15,11 @@ use App\Traits\Filament\CanCustomizeRelations;
use App\Traits\Filament\CanModifyForm;
use App\Traits\Filament\CanModifyTable;
use App\Traits\Filament\HasLimitBadge;
use Exception;
use Filament\Facades\Filament;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\PageRegistration;
use Filament\Resources\Resource;
use Filament\Tables\Actions\DeleteAction;
@ -122,7 +124,23 @@ class DatabaseResource extends Resource
ViewAction::make()
->modalHeading(fn (Database $database) => trans('server/database.viewing', ['database' => $database->database])),
DeleteAction::make()
->using(fn (Database $database, DatabaseManagementService $service) => $service->delete($database)),
->using(function (Database $database, DatabaseManagementService $service) {
try {
$service->delete($database);
Notification::make()
->title(trans('server/database.delete_notification', ['database' => $database->database]))
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title(trans('server/database.delete_notification_fail', ['database' => $database->database]))
->danger()
->send();
report($exception);
}
}),
]);
}

View File

@ -8,6 +8,7 @@ use App\Models\Server;
use App\Services\Databases\DatabaseManagementService;
use App\Traits\Filament\CanCustomizeHeaderActions;
use App\Traits\Filament\CanCustomizeHeaderWidgets;
use Exception;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Actions\CreateAction;
@ -15,8 +16,10 @@ use Filament\Facades\Filament;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Filament\Support\Enums\IconSize;
use Illuminate\Support\Str;
class ListDatabases extends ListRecords
{
@ -62,12 +65,24 @@ class ListDatabases extends ListRecords
]),
])
->action(function ($data, DatabaseManagementService $service) use ($server) {
if (empty($data['database'])) {
$data['database'] = str_random(12);
}
$data['database'] = 's'. $server->id . '_' . $data['database'];
$data['database'] ??= Str::random(12);
$data['database'] = $service->generateUniqueDatabaseName($data['database'], $server->id);
$service->create($server, $data);
try {
$service->create($server, $data);
Notification::make()
->title(trans('server/database.create_notification', ['database' => $data['database']]))
->success()
->send();
} catch (Exception $exception) {
Notification::make()
->title(trans('server/database.create_notification_fail', ['database' => $data['database']]))
->danger()
->send();
report($exception);
}
}),
];
}

View File

@ -193,10 +193,10 @@ class ListFiles extends ListRecords
->required()
->live(),
Placeholder::make('new_location')
->content(fn (Get $get, File $file) => resolve_path('./' . join_paths($this->path, $get('location') ?? '/', $file->name))),
->content(fn (Get $get, File $file) => resolve_path(join_paths($this->path, $get('location') ?? '/', $file->name))),
])
->action(function ($data, File $file) {
$location = rtrim($data['location'], '/');
$location = $data['location'];
$files = [['to' => join_paths($location, $file->name), 'from' => $file->name]];
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
@ -353,10 +353,10 @@ class ListFiles extends ListRecords
->required()
->live(),
Placeholder::make('new_location')
->content(fn (Get $get) => resolve_path('./' . join_paths($this->path, $get('location') ?? ''))),
->content(fn (Get $get) => resolve_path(join_paths($this->path, $get('location') ?? ''))),
])
->action(function (Collection $files, $data) {
$location = rtrim($data['location'], '/');
$location = $data['location'];
$files = $files->map(fn ($file) => ['to' => join_paths($location, $file['name']), 'from' => $file['name']])->toArray();
$this->getDaemonFileRepository()->renameFiles($this->path, $files);
@ -432,11 +432,12 @@ class ListFiles extends ListRecords
->modalSubmitActionLabel(trans('server/file.actions.new_file.create'))
->action(function ($data) {
$path = join_paths($this->path, $data['name']);
try {
$this->getDaemonFileRepository()->putContent($path, $data['editor'] ?? '');
Activity::event('server:file.write')
->property('file', join_paths($path, $data['name']))
->property('file', $path)
->log();
} catch (FileExistsException) {
AlertBanner::make('file_already_exists')

View File

@ -4,6 +4,7 @@ namespace App\Helpers;
use Carbon\Carbon;
use Cron\CronExpression;
use Illuminate\Support\Str;
use Illuminate\Support\ViewErrorBag;
class Utilities
@ -14,7 +15,7 @@ class Utilities
*/
public static function randomStringWithSpecialCharacters(int $length = 16): string
{
$string = str_random($length);
$string = Str::random($length);
// Given a random string of characters, randomly loop through the characters and replace some
// with special characters to avoid issues with MySQL password requirements on some servers.
try {

View File

@ -6,7 +6,6 @@ use Illuminate\Http\Response;
use App\Models\Server;
use App\Models\Database;
use Illuminate\Http\JsonResponse;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Databases\DatabaseManagementService;
use App\Transformers\Api\Application\ServerDatabaseTransformer;
use App\Http\Controllers\Api\Application\ApplicationApiController;
@ -24,7 +23,6 @@ class DatabaseController extends ApplicationApiController
*/
public function __construct(
private DatabaseManagementService $databaseManagementService,
private DatabasePasswordService $databasePasswordService
) {
parent::__construct();
}
@ -66,7 +64,7 @@ class DatabaseController extends ApplicationApiController
*/
public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): JsonResponse
{
$this->databasePasswordService->handle($database);
$this->databaseManagementService->rotatePassword($database);
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
}

View File

@ -6,7 +6,6 @@ use Illuminate\Http\Response;
use App\Models\Server;
use App\Models\Database;
use App\Facades\Activity;
use App\Services\Databases\DatabasePasswordService;
use App\Transformers\Api\Client\DatabaseTransformer;
use App\Services\Databases\DatabaseManagementService;
use App\Services\Databases\DeployServerDatabaseService;
@ -26,7 +25,6 @@ class DatabaseController extends ClientApiController
public function __construct(
private DeployServerDatabaseService $deployDatabaseService,
private DatabaseManagementService $managementService,
private DatabasePasswordService $passwordService
) {
parent::__construct();
}
@ -83,7 +81,7 @@ class DatabaseController extends ClientApiController
*/
public function rotatePassword(RotatePasswordRequest $request, Server $server, Database $database): array
{
$this->passwordService->handle($database);
$this->managementService->rotatePassword($database);
$database->refresh();
Activity::event('server:database.rotate-password')

View File

@ -5,9 +5,9 @@ namespace App\Http\Controllers\Api\Client\Servers;
use Illuminate\Http\Response;
use App\Models\Server;
use App\Facades\Activity;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Http\Controllers\Api\Client\ClientApiController;
use App\Http\Requests\Api\Client\Servers\SendPowerRequest;
use App\Repositories\Daemon\DaemonServerRepository;
use Dedoc\Scramble\Attributes\Group;
use Illuminate\Http\Client\ConnectionException;
@ -17,7 +17,7 @@ class PowerController extends ClientApiController
/**
* PowerController constructor.
*/
public function __construct(private DaemonPowerRepository $repository)
public function __construct(private DaemonServerRepository $repository)
{
parent::__construct();
}
@ -31,7 +31,7 @@ class PowerController extends ClientApiController
*/
public function index(SendPowerRequest $request, Server $server): Response
{
$this->repository->setServer($server)->send(
$this->repository->setServer($server)->power(
$request->input('signal')
);

View File

@ -5,11 +5,11 @@ namespace App\Jobs\Schedule;
use App\Jobs\Job;
use Carbon\CarbonImmutable;
use App\Models\Task;
use App\Repositories\Daemon\DaemonServerRepository;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Services\Backups\InitiateBackupService;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Services\Files\DeleteFilesService;
use Exception;
use Illuminate\Http\Client\ConnectionException;
@ -31,7 +31,7 @@ class RunTaskJob extends Job implements ShouldQueue
*/
public function handle(
InitiateBackupService $backupService,
DaemonPowerRepository $powerRepository,
DaemonServerRepository $serverRepository,
DeleteFilesService $deleteFilesService
): void {
// Do not process a task that is not set to active, unless it's been manually triggered.
@ -57,7 +57,7 @@ class RunTaskJob extends Job implements ShouldQueue
try {
switch ($this->task->action) {
case Task::ACTION_POWER:
$powerRepository->setServer($server)->send($this->task->payload);
$serverRepository->setServer($server)->power($this->task->payload);
break;
case Task::ACTION_COMMAND:
$server->send($this->task->payload);

View File

@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use PDOException;
/**
* @property int $id
@ -97,71 +98,97 @@ class Database extends Model implements Validatable
}
/**
* @throws PDOException
*
* Run the provided statement against the database on a given connection.
*/
private function run(string $statement): bool
private function run(string $statement): void
{
return $this->host->buildConnection()->statement($statement);
$this->host->buildConnection()->statement($statement);
}
/**
* @throws PDOException
*
* Create a new database on a given connection.
*/
public function createDatabase(string $database): bool
public function createDatabase(): self
{
return $this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database));
$this->run(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $this->database));
return $this;
}
/**
* @throws PDOException
*
* Create a new database user on a given connection.
*/
public function createUser(string $username, string $remote, string $password, ?int $max_connections): bool
public function createUser(): self
{
$args = [$username, $remote, $password];
$args = [$this->username, $this->remote, $this->password];
$command = 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'';
if (!empty($max_connections)) {
$args[] = $max_connections;
if (!empty($this->max_connections)) {
$args[] = $this->max_connections;
$command .= ' WITH MAX_USER_CONNECTIONS %s';
}
return $this->run(sprintf($command, ...$args));
$this->run(sprintf($command, ...$args));
return $this;
}
/**
* @throws PDOException
*
* Give a specific user access to a given database.
*/
public function assignUserToDatabase(string $database, string $username, string $remote): bool
public function assignUserToDatabase(): self
{
return $this->run(sprintf(
$this->run(sprintf(
'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, REFERENCES, INDEX, LOCK TABLES, CREATE ROUTINE, ALTER ROUTINE, EXECUTE, CREATE TEMPORARY TABLES, CREATE VIEW, SHOW VIEW, EVENT, TRIGGER ON `%s`.* TO `%s`@`%s`',
$database,
$username,
$remote
$this->database,
$this->username,
$this->remote
));
return $this;
}
/**
* @throws PDOException
*
* Flush the privileges for a given connection.
*/
public function flush(): bool
public function flushPrivileges(): self
{
return $this->run('FLUSH PRIVILEGES');
$this->run('FLUSH PRIVILEGES');
return $this;
}
/**
* @throws PDOException
*
* Drop a given database on a specific connection.
*/
public function dropDatabase(string $database): bool
public function dropDatabase(): self
{
return $this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $database));
$this->run(sprintf('DROP DATABASE IF EXISTS `%s`', $this->database));
return $this;
}
/**
* @throws PDOException
*
* Drop a given user on a specific connection.
*/
public function dropUser(string $username, string $remote): bool
public function dropUser(): self
{
return $this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote));
$this->run(sprintf('DROP USER IF EXISTS `%s`@`%s`', $this->username, $this->remote));
return $this;
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace App\Repositories\Daemon;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
class DaemonPowerRepository extends DaemonRepository
{
/**
* Sends a power action to the server instance.
*
* @throws ConnectionException
*/
public function send(string $action): Response
{
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/power",
['action' => $action],
);
}
}

View File

@ -8,6 +8,7 @@ use Exception;
use Filament\Notifications\Notification;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\Client\Response;
class DaemonServerRepository extends DaemonRepository
{
@ -149,4 +150,16 @@ class DaemonServerRepository extends DaemonRepository
->throw()
->json('data');
}
/**
* Sends a power action to the server instance.
*
* @throws ConnectionException
*/
public function power(string $action): Response
{
return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/power",
['action' => $action],
);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services\Api;
use App\Models\ApiKey;
use Illuminate\Support\Str;
class KeyCreationService
{
@ -33,7 +34,7 @@ class KeyCreationService
$data = array_merge($data, [
'key_type' => $this->keyType,
'identifier' => ApiKey::generateTokenIdentifier($this->keyType),
'token' => str_random(ApiKey::KEY_LENGTH),
'token' => Str::random(ApiKey::KEY_LENGTH),
]);
if ($this->keyType !== ApiKey::TYPE_APPLICATION) {

View File

@ -10,6 +10,7 @@ use Illuminate\Database\ConnectionInterface;
use App\Exceptions\Repository\DuplicateDatabaseNameException;
use App\Exceptions\Service\Database\TooManyDatabasesException;
use App\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
use Illuminate\Support\Str;
class DatabaseManagementService
{
@ -85,22 +86,18 @@ class DatabaseManagementService
$data = array_merge($data, [
'server_id' => $server->id,
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
'username' => sprintf('u%d_%s', $server->id, Str::random(10)),
'password' => Utilities::randomStringWithSpecialCharacters(24),
]);
return $this->connection->transaction(function () use ($data) {
$database = $this->createModel($data);
$database->createDatabase($database->database);
$database->createUser(
$database->username,
$database->remote,
$database->password,
$database->max_connections
);
$database->assignUserToDatabase($database->database, $database->username, $database->remote);
$database->flush();
$database
->createDatabase()
->createUser()
->assignUserToDatabase()
->flushPrivileges();
Activity::event('server:database.create')
->subject($database)
@ -114,14 +111,15 @@ class DatabaseManagementService
/**
* Delete a database from the given host server.
*
* @throws \Exception
* @throws \Throwable
*/
public function delete(Database $database): ?bool
{
return $this->connection->transaction(function () use ($database) {
$database->dropDatabase($database->database);
$database->dropUser($database->username, $database->remote);
$database->flush();
$database
->dropDatabase()
->dropUser()
->flushPrivileges();
Activity::event('server:database.delete')
->subject($database)
@ -132,6 +130,28 @@ class DatabaseManagementService
});
}
/**
* Updates a password for a given database.
*
* @throws \Exception
*/
public function rotatePassword(Database $database): void
{
$password = Utilities::randomStringWithSpecialCharacters(24);
$this->connection->transaction(function () use ($database, $password) {
$database->update([
'password' => $password,
]);
$database
->dropUser()
->createUser()
->assignUserToDatabase()
->flushPrivileges();
});
}
/**
* Create the database if there is not an identical match in the DB. While you can technically
* have the same name across multiple hosts, for the sake of keeping this logic easy to understand

View File

@ -1,40 +0,0 @@
<?php
namespace App\Services\Databases;
use App\Models\Database;
use App\Helpers\Utilities;
use Illuminate\Database\ConnectionInterface;
class DatabasePasswordService
{
/**
* DatabasePasswordService constructor.
*/
public function __construct(
private ConnectionInterface $connection,
) {}
/**
* Updates a password for a given database.
*/
public function handle(Database|int $database): void
{
if (is_int($database)) {
$database = Database::query()->findOrFail($database);
}
$password = Utilities::randomStringWithSpecialCharacters(24);
$this->connection->transaction(function () use ($database, $password) {
$database->update([
'password' => $password,
]);
$database->dropUser($database->username, $database->remote);
$database->createUser($database->username, $database->remote, $password, $database->max_connections);
$database->assignUserToDatabase($database->database, $database->username, $database->remote);
$database->flush();
});
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services\Users;
use App\Models\User;
use Illuminate\Support\Str;
use PragmaRX\Google2FA\Google2FA;
use Illuminate\Database\ConnectionInterface;
use App\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid;
@ -48,7 +49,7 @@ class ToggleTwoFactorService
if ((!$toggleState && !$user->use_totp) || $toggleState) {
$user->recoveryTokens()->delete();
for ($i = 0; $i < 10; $i++) {
$token = str_random(10);
$token = Str::random(10);
$user->recoveryTokens()->forceCreate([
'token' => password_hash($token, PASSWORD_DEFAULT),
]);

View File

@ -37,7 +37,7 @@ class UserCreationService
$this->connection->beginTransaction();
if (empty($data['password'])) {
$generateResetToken = true;
$data['password'] = $this->hasher->make(str_random(30));
$data['password'] = $this->hasher->make(Str::random(30));
}
$isRootAdmin = array_key_exists('root_admin', $data) && $data['root_admin'];

View File

@ -52,11 +52,16 @@ if (!function_exists('convert_bytes_to_readable')) {
if (!function_exists('join_paths')) {
function join_paths(string $base, string ...$paths): string
{
if ($base === '/') {
return str_replace('//', '', implode('/', $paths));
$base = trim($base, '/');
$paths = array_map(fn (string $path) => trim($path, '/'), $paths);
$paths = array_filter($paths, fn (string $path) => strlen($path) > 0);
if (empty($base)) {
return implode('/', $paths);
}
return str_replace('//', '', $base . '/' . implode('/', $paths));
return $base . '/' . implode('/', $paths);
}
}

View File

@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use App\Contracts\Repository\DaemonKeyRepositoryInterface;
use Illuminate\Support\Str;
return new class extends Migration
{
@ -21,7 +22,7 @@ return new class extends Migration
$inserts[] = [
'user_id' => $server->owner_id,
'server_id' => $server->id,
'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40),
'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . Str::random(40),
'expires_at' => Carbon::now()->addMinutes(config('panel.api.key_expire_time', 720))->toDateTimeString(),
'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => Carbon::now()->toDateTimeString(),

View File

@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use App\Contracts\Repository\DaemonKeyRepositoryInterface;
use Illuminate\Support\Str;
return new class extends Migration
{
@ -19,7 +20,7 @@ return new class extends Migration
$inserts[] = [
'user_id' => $subuser->user_id,
'server_id' => $subuser->server_id,
'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40),
'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . Str::random(40),
'expires_at' => Carbon::now()->addMinutes(config('panel.api.key_expire_time', 720))->toDateTimeString(),
'created_at' => Carbon::now()->toDateTimeString(),
'updated_at' => Carbon::now()->toDateTimeString(),

View File

@ -1,5 +1,6 @@
<?php
use Illuminate\Support\Str;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
@ -51,7 +52,7 @@ return new class extends Migration
DB::transaction(function () {
DB::table('service_options')->select(['id', 'tag'])->get()->each(function ($option) {
DB::table('service_options')->where('id', $option->id)->update([
'tag' => str_random(10),
'tag' => Str::random(10),
]);
});
});

View File

@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Support\Str;
return new class extends Migration
{
@ -19,7 +20,7 @@ return new class extends Migration
try {
$decrypted = Crypt::decrypt($item->secret);
} catch (DecryptException $exception) {
$decrypted = str_random(32);
$decrypted = Str::random(32);
} finally {
DB::table('api_keys')->where('id', $item->id)->update([
'secret' => $decrypted,
@ -66,7 +67,7 @@ return new class extends Migration
DB::transaction(function () {
DB::table('api_keys')->get()->each(function ($item) {
DB::table('api_keys')->where('id', $item->id)->update([
'public' => str_random(16),
'public' => Str::random(16),
'secret' => Crypt::encrypt($item->secret),
]);
});

View File

@ -19,4 +19,8 @@ return [
'database_host' => 'Database Host',
'database_host_select' => 'Select Database Host',
'jdbc' => 'JDBC Connection String',
'create_notification' => 'Created :database',
'create_notification_fail' => 'Failed to create :database',
'delete_notification' => 'Deleted :database',
'delete_notification_fail' => 'Failed to delete :database',
];

View File

@ -5,7 +5,6 @@ namespace App\Tests\Integration\Api\Client\Server\Database;
use App\Models\Subuser;
use App\Models\Database;
use App\Models\DatabaseHost;
use App\Services\Databases\DatabasePasswordService;
use App\Services\Databases\DatabaseManagementService;
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
@ -33,8 +32,8 @@ class DatabaseAuthorizationTest extends ClientApiIntegrationTestCase
$database3 = Database::factory()->create(['server_id' => $server3->id, 'database_host_id' => $host->id]);
$this
->mock($method === 'POST' ? DatabasePasswordService::class : DatabaseManagementService::class)
->expects($method === 'POST' ? 'handle' : 'delete')
->mock(DatabaseManagementService::class)
->expects($method === 'POST' ? 'rotatePassword' : 'delete')
->andReturn($method === 'POST' ? 'foo' : null);
// This is the only valid call for this test, accessing the database for the same

View File

@ -4,7 +4,7 @@ namespace App\Tests\Integration\Api\Client\Server;
use Illuminate\Http\Response;
use App\Models\Permission;
use App\Repositories\Daemon\DaemonPowerRepository;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
@ -49,8 +49,8 @@ class PowerControllerTest extends ClientApiIntegrationTestCase
#[DataProvider('validPowerActionDataProvider')]
public function test_action_can_be_sent_to_server(string $action, string $permission): void
{
$service = \Mockery::mock(DaemonPowerRepository::class);
$this->app->instance(DaemonPowerRepository::class, $service);
$service = \Mockery::mock(DaemonServerRepository::class);
$this->app->instance(DaemonServerRepository::class, $service);
[$user, $server] = $this->generateTestAccount([$permission]);
@ -60,7 +60,7 @@ class PowerControllerTest extends ClientApiIntegrationTestCase
}))
->andReturnSelf()
->getMock()
->expects('send')
->expects('power')
->with(trim($action));
$this->actingAs($user)

View File

@ -10,8 +10,8 @@ use App\Models\Server;
use App\Models\Schedule;
use Illuminate\Support\Facades\Bus;
use App\Jobs\Schedule\RunTaskJob;
use App\Repositories\Daemon\DaemonServerRepository;
use App\Tests\Integration\IntegrationTestCase;
use App\Repositories\Daemon\DaemonPowerRepository;
use Illuminate\Http\Client\ConnectionException;
use PHPUnit\Framework\Attributes\DataProvider;
@ -84,13 +84,13 @@ class RunTaskJobTest extends IntegrationTestCase
'continue_on_failure' => false,
]);
$mock = \Mockery::mock(DaemonPowerRepository::class);
$this->instance(DaemonPowerRepository::class, $mock);
$mock = \Mockery::mock(DaemonServerRepository::class);
$this->instance(DaemonServerRepository::class, $mock);
$mock->expects('setServer')->with(\Mockery::on(function ($value) use ($server) {
return $value instanceof Server && $value->id === $server->id;
}))->andReturnSelf();
$mock->expects('send')->with('start');
$mock->expects('power')->with('start');
Bus::dispatchSync(new RunTaskJob($task, $isManualRun));
@ -117,10 +117,10 @@ class RunTaskJobTest extends IntegrationTestCase
'continue_on_failure' => $continueOnFailure,
]);
$mock = \Mockery::mock(DaemonPowerRepository::class);
$this->instance(DaemonPowerRepository::class, $mock);
$mock = \Mockery::mock(DaemonServerRepository::class);
$this->instance(DaemonServerRepository::class, $mock);
$mock->expects('setServer->send')->andThrow(new ConnectionException());
$mock->expects('setServer->power')->andThrow(new ConnectionException());
if (!$continueOnFailure) {
$this->expectException(ConnectionException::class);