diff --git a/app/Filament/Pages/Dashboard.php b/app/Filament/Admin/Pages/Dashboard.php similarity index 94% rename from app/Filament/Pages/Dashboard.php rename to app/Filament/Admin/Pages/Dashboard.php index 62e0b10b47..b3a56b27cd 100644 --- a/app/Filament/Pages/Dashboard.php +++ b/app/Filament/Admin/Pages/Dashboard.php @@ -1,9 +1,9 @@ revealable() ->maxLength(255) ->required(), - Select::make('node_id') + Select::make('node_ids') + ->multiple() ->searchable() ->preload() ->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.') - ->label('Linked Node') - ->relationship('node', 'name'), + ->label('Linked Nodes') + ->relationship('nodes', 'name'), ]), ]); } diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php similarity index 91% rename from app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php rename to app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php index c3e70c9ecd..92aac1dfe7 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php @@ -1,9 +1,9 @@ password() ->revealable() ->maxLength(255), - Select::make('node_id') + Select::make('nodes') + ->multiple() ->searchable() ->preload() ->helperText('This setting only defaults to this database host when adding a database to a server on the selected node.') - ->label('Linked Node') - ->relationship('node', 'name'), + ->label('Linked Nodes') + ->relationship('nodes', 'name'), ]), ]); } diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php similarity index 91% rename from app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php rename to app/Filament/Admin/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php index c5db9ea855..31837d14c9 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/Pages/ListDatabaseHosts.php @@ -1,8 +1,8 @@ counts('databases') ->icon('tabler-database') ->label('Databases'), - TextColumn::make('node.name') + TextColumn::make('nodes.name') ->icon('tabler-server-2') + ->badge() ->placeholder('No Nodes') ->sortable(), ]) diff --git a/app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php b/app/Filament/Admin/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php similarity index 98% rename from app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php rename to app/Filament/Admin/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php index 448245e421..ecc13de502 100644 --- a/app/Filament/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php +++ b/app/Filament/Admin/Resources/DatabaseHostResource/RelationManagers/DatabasesRelationManager.php @@ -1,6 +1,6 @@ label('Database Host') ->required() ->placeholder('Select Database Host') - ->relationship('node.databaseHosts', 'name') + ->relationship('node.databaseHosts', 'name', + fn (Builder $query, Server $server) => $query->whereRelation('nodes', 'nodes.id', $server->node_id)) ->default(fn () => (DatabaseHost::query()->first())?->id) ->selectablePlaceholder(false), TextInput::make('database') diff --git a/app/Filament/Resources/ServerResource/Pages/ListServers.php b/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php similarity index 97% rename from app/Filament/Resources/ServerResource/Pages/ListServers.php rename to app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php index d43e09cfef..66bca87e35 100644 --- a/app/Filament/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php @@ -1,9 +1,9 @@ headerActions([ Action::make('Create') ->disabled(fn (Get $get) => $get('description') === null) - ->successRedirectUrl(route('filament.admin.auth.profile', ['tab' => '-api-keys-tab'])) + ->successRedirectUrl(self::getUrl(['tab' => '-api-keys-tab'])) ->action(function (Get $get, Action $action, User $user) { $token = $user->createToken( $get('description'), $get('allowed_ips'), ); + Activity::event('user:api-key.create') ->subject($token->accessToken) ->property('identifier', $token->accessToken->identifier) ->log(); + + Notification::make() + ->title('API Key created') + ->body($token->accessToken->identifier . $token->plainTextToken) + ->persistent() + ->success() + ->send(); + $action->success(); }), ]), diff --git a/app/Filament/Pages/Installer/PanelInstaller.php b/app/Filament/Pages/Installer/PanelInstaller.php index 0ae31c2dd9..27b657ada1 100644 --- a/app/Filament/Pages/Installer/PanelInstaller.php +++ b/app/Filament/Pages/Installer/PanelInstaller.php @@ -2,7 +2,7 @@ namespace App\Filament\Pages\Installer; -use App\Filament\Pages\Dashboard; +use App\Filament\Admin\Pages\Dashboard; use App\Filament\Pages\Installer\Steps\CacheStep; use App\Filament\Pages\Installer\Steps\DatabaseStep; use App\Filament\Pages\Installer\Steps\EnvironmentStep; diff --git a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php index dc93578da1..917cc91269 100644 --- a/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php +++ b/app/Filament/Server/Resources/DatabaseResource/Pages/ListDatabases.php @@ -14,6 +14,7 @@ use Filament\Facades\Filament; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Grid; +use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Form; use Filament\Forms\Get; @@ -103,6 +104,11 @@ protected function getHeaderActions(): array Grid::make() ->columns(3) ->schema([ + Select::make('database_host_id') + ->label('Database Host') + ->required() + ->placeholder('Select Database Host') + ->options(fn () => $server->node->databaseHosts->mapWithKeys(fn (DatabaseHost $databaseHost) => [$databaseHost->id => $databaseHost->name])), TextInput::make('database') ->columnSpan(2) ->label('Database Name') @@ -119,8 +125,6 @@ protected function getHeaderActions(): array if (empty($data['database'])) { $data['database'] = str_random(12); } - - $data['database_host_id'] = DatabaseHost::where('node_id', $server->node_id)->first()->id; $data['database'] = 's'. $server->id . '_' . $data['database']; $service->create($server, $data); diff --git a/app/Http/Controllers/Auth/OAuthController.php b/app/Http/Controllers/Auth/OAuthController.php index 9df0978c12..87403d1620 100644 --- a/app/Http/Controllers/Auth/OAuthController.php +++ b/app/Http/Controllers/Auth/OAuthController.php @@ -2,7 +2,7 @@ namespace App\Http\Controllers\Auth; -use App\Filament\Resources\UserResource\Pages\EditProfile; +use App\Filament\Pages\Auth\EditProfile; use Filament\Notifications\Notification; use Illuminate\Auth\AuthManager; use Illuminate\Http\RedirectResponse; diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 66e37c23e2..325211a508 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -2,8 +2,8 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * @property int $id @@ -39,7 +39,7 @@ class DatabaseHost extends Model * Fields that are mass assignable. */ protected $fillable = [ - 'name', 'host', 'port', 'username', 'password', 'max_databases', 'node_id', + 'name', 'host', 'port', 'username', 'password', 'max_databases', ]; /** @@ -51,7 +51,8 @@ class DatabaseHost extends Model 'port' => 'required|numeric|between:1,65535', 'username' => 'required|string|max:32', 'password' => 'nullable|string', - 'node_id' => 'sometimes|nullable|integer|exists:nodes,id', + 'node_ids' => 'nullable|array', + 'node_ids.*' => 'required|integer,exists:nodes,id', ]; protected function casts(): array @@ -59,7 +60,6 @@ protected function casts(): array return [ 'id' => 'integer', 'max_databases' => 'integer', - 'node_id' => 'integer', 'password' => 'encrypted', 'created_at' => 'immutable_datetime', 'updated_at' => 'immutable_datetime', @@ -71,12 +71,9 @@ public function getRouteKeyName(): string return 'id'; } - /** - * Gets the node associated with a database host. - */ - public function node(): BelongsTo + public function nodes(): BelongsToMany { - return $this->belongsTo(Node::class); + return $this->belongsToMany(Node::class); } /** diff --git a/app/Models/Node.php b/app/Models/Node.php index e13f0a592f..7d10006f1f 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -5,6 +5,7 @@ use App\Exceptions\Service\HasActiveServersException; use App\Repositories\Daemon\DaemonConfigurationRepository; use Exception; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Notifications\Notifiable; @@ -243,9 +244,9 @@ public function allocations(): HasMany return $this->hasMany(Allocation::class); } - public function databaseHosts(): HasMany + public function databaseHosts(): BelongsToMany { - return $this->hasMany(DatabaseHost::class); + return $this->belongsToMany(DatabaseHost::class); } /** diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 9b15afdf83..73ca6424ee 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -3,7 +3,7 @@ namespace App\Providers\Filament; use App\Filament\Pages\Auth\Login; -use App\Filament\Resources\UserResource\Pages\EditProfile; +use App\Filament\Pages\Auth\EditProfile; use App\Http\Middleware\LanguageMiddleware; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; diff --git a/app/Providers/Filament/AppPanelProvider.php b/app/Providers/Filament/AppPanelProvider.php index d5bed4f806..3bd0e805e2 100644 --- a/app/Providers/Filament/AppPanelProvider.php +++ b/app/Providers/Filament/AppPanelProvider.php @@ -3,7 +3,7 @@ namespace App\Providers\Filament; use App\Filament\Pages\Auth\Login; -use App\Filament\Resources\UserResource\Pages\EditProfile; +use App\Filament\Pages\Auth\EditProfile; use Filament\Facades\Filament; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\DisableBladeIconComponents; diff --git a/app/Providers/Filament/ServerPanelProvider.php b/app/Providers/Filament/ServerPanelProvider.php index 6ebfe10bf7..9682a64651 100644 --- a/app/Providers/Filament/ServerPanelProvider.php +++ b/app/Providers/Filament/ServerPanelProvider.php @@ -4,8 +4,8 @@ use App\Filament\App\Resources\ServerResource\Pages\ListServers; use App\Filament\Pages\Auth\Login; -use App\Filament\Resources\ServerResource\Pages\EditServer; -use App\Filament\Resources\UserResource\Pages\EditProfile; +use App\Filament\Admin\Resources\ServerResource\Pages\EditServer; +use App\Filament\Pages\Auth\EditProfile; use App\Http\Middleware\Activity\ServerSubject; use App\Models\Server; use Filament\Facades\Filament; diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php index 40ffedbbca..6cf60fb190 100644 --- a/app/Services/Databases/DeployServerDatabaseService.php +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -25,15 +25,15 @@ public function handle(Server $server, array $data): Database Assert::notEmpty($data['database'] ?? null); Assert::notEmpty($data['remote'] ?? null); - $hosts = DatabaseHost::query()->get()->toBase(); + $hosts = DatabaseHost::query()->get(); if ($hosts->isEmpty()) { throw new NoSuitableDatabaseHostException(); - } else { - $nodeHosts = $hosts->where('node_id', $server->node_id)->toBase(); + } - if ($nodeHosts->isEmpty() && !config('panel.client_features.databases.allow_random')) { - throw new NoSuitableDatabaseHostException(); - } + $nodeHosts = $server->node->databaseHosts()->get(); + // TODO: @areyouscared remove allow random feature for database hosts + if ($nodeHosts->isEmpty() && !config('panel.client_features.databases.allow_random')) { + throw new NoSuitableDatabaseHostException(); } return $this->managementService->create($server, [ diff --git a/app/Services/Databases/Hosts/HostCreationService.php b/app/Services/Databases/Hosts/HostCreationService.php index d4d4264f17..ff38cb5c97 100644 --- a/app/Services/Databases/Hosts/HostCreationService.php +++ b/app/Services/Databases/Hosts/HostCreationService.php @@ -33,9 +33,10 @@ public function handle(array $data): DatabaseHost 'port' => array_get($data, 'port'), 'username' => array_get($data, 'username'), 'max_databases' => array_get($data, 'max_databases'), - 'node_id' => array_get($data, 'node_id'), ]); + $host->nodes()->sync(array_get($data, 'node_ids', [])); + // Confirm access using the provided credentials before saving data. $this->dynamic->set('dynamic', $host); $this->databaseManager->connection('dynamic')->getPdo(); diff --git a/app/Transformers/Api/Application/DatabaseHostTransformer.php b/app/Transformers/Api/Application/DatabaseHostTransformer.php index 0402ff7e95..8f3453f6a3 100644 --- a/app/Transformers/Api/Application/DatabaseHostTransformer.php +++ b/app/Transformers/Api/Application/DatabaseHostTransformer.php @@ -5,7 +5,6 @@ use App\Models\Node; use App\Models\Database; use App\Models\DatabaseHost; -use League\Fractal\Resource\Item; use League\Fractal\Resource\Collection; use League\Fractal\Resource\NullResource; @@ -13,7 +12,7 @@ class DatabaseHostTransformer extends BaseTransformer { protected array $availableIncludes = [ 'databases', - 'node', + 'nodes', ]; /** @@ -35,7 +34,6 @@ public function transform(DatabaseHost $model): array 'host' => $model->host, 'port' => $model->port, 'username' => $model->username, - 'node' => $model->node_id, 'created_at' => $model->created_at->toAtomString(), 'updated_at' => $model->updated_at->toAtomString(), ]; @@ -56,16 +54,16 @@ public function includeDatabases(DatabaseHost $model): Collection|NullResource } /** - * Include the node associated with this host. + * Include the nodes associated with this host. */ - public function includeNode(DatabaseHost $model): Item|NullResource + public function includeNodes(DatabaseHost $model): Collection|NullResource { if (!$this->authorize(Node::RESOURCE_NAME)) { return $this->null(); } - $model->loadMissing('node'); + $model->loadMissing('nodes'); - return $this->item($model->getRelation('node'), $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME); + return $this->collection($model->getRelation('nodes'), $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME); } } diff --git a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php index 777889565c..e1af0b0a31 100644 --- a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php +++ b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php @@ -34,6 +34,9 @@ public function up(): void if (Schema::getConnection()->getDriverName() !== 'sqlite') { $table->dropIndex('permissions_server_id_foreign'); $table->dropIndex('permissions_user_id_foreign'); + } else { + $table->dropForeign(['server_id']); + $table->dropForeign(['user_id']); } $table->dropColumn('server_id'); diff --git a/database/migrations/2020_09_13_110007_drop_packs_from_servers.php b/database/migrations/2020_09_13_110007_drop_packs_from_servers.php index fc87382b98..39f048b373 100644 --- a/database/migrations/2020_09_13_110007_drop_packs_from_servers.php +++ b/database/migrations/2020_09_13_110007_drop_packs_from_servers.php @@ -13,7 +13,6 @@ public function up(): void { Schema::table('servers', function (Blueprint $table) { $table->dropForeign(['pack_id']); - $table->dropColumn('pack_id'); }); } diff --git a/database/migrations/2024_03_12_154408_remove_nests_table.php b/database/migrations/2024_03_12_154408_remove_nests_table.php index 2b5542f23f..42d1315b91 100644 --- a/database/migrations/2024_03_12_154408_remove_nests_table.php +++ b/database/migrations/2024_03_12_154408_remove_nests_table.php @@ -33,6 +33,7 @@ public function up(): void } else { $table->dropForeign(['nest_id']); } + $table->dropColumn('nest_id'); }); diff --git a/database/migrations/2024_10_31_203540_change_database_hosts_to_belong_to_many_nodes.php b/database/migrations/2024_10_31_203540_change_database_hosts_to_belong_to_many_nodes.php new file mode 100644 index 0000000000..2fc831cb18 --- /dev/null +++ b/database/migrations/2024_10_31_203540_change_database_hosts_to_belong_to_many_nodes.php @@ -0,0 +1,51 @@ +id(); + $table->unsignedInteger('node_id'); + $table->foreign('node_id')->references('id')->on('nodes'); + $table->unsignedInteger('database_host_id'); + $table->foreign('database_host_id')->references('id')->on('database_hosts'); + $table->timestamps(); + }); + + $databaseNodes = DB::table('database_hosts')->whereNotNull('node_id')->get(); + $newJoinEntries = $databaseNodes->map(fn ($record) => [ + 'node_id' => $record->node_id, + 'database_host_id' => $record->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('database_host_node')->insert($newJoinEntries->toArray()); + + Schema::table('database_hosts', function (Blueprint $table) { + $table->dropForeign(['node_id']); + $table->dropColumn('node_id'); + }); + } + + public function down(): void + { + Schema::table('database_hosts', function (Blueprint $table) { + $table->unsignedInteger('node_id')->nullable(); + $table->foreign('node_id')->references('id')->on('nodes'); + }); + + foreach (DB::table('database_host_node')->get() as $record) { + DB::table('database_hosts') + ->where('id', $record->database_host_id) + ->update(['node_id' => $record->node_id]); + } + + Schema::drop('database_host_node'); + } +}; diff --git a/tests/Integration/Services/Databases/DatabaseManagementServiceTest.php b/tests/Integration/Services/Databases/DatabaseManagementServiceTest.php index 060ed9f10f..834e6e345e 100644 --- a/tests/Integration/Services/Databases/DatabaseManagementServiceTest.php +++ b/tests/Integration/Services/Databases/DatabaseManagementServiceTest.php @@ -52,7 +52,7 @@ public function testExceptionIsThrownIfClientDatabasesAreNotEnabled(): void public function testDatabaseCannotBeCreatedIfServerHasReachedLimit(): void { $server = $this->createServerModel(['database_limit' => 2]); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); Database::factory()->times(2)->create(['server_id' => $server->id, 'database_host_id' => $host->id]); @@ -84,8 +84,8 @@ public function testCreatingDatabaseWithIdenticalNameTriggersAnException(): void $server = $this->createServerModel(); $name = DatabaseManagementService::generateUniqueDatabaseName('something', $server->id); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); - $host2 = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); + $host2 = DatabaseHost::factory()->recycle($server->node)->create(); Database::factory()->create([ 'database' => $name, 'database_host_id' => $host->id, @@ -117,7 +117,7 @@ public function testServerDatabaseCanBeCreated(): void $server = $this->createServerModel(); $name = DatabaseManagementService::generateUniqueDatabaseName('something', $server->id); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); $username = null; $secondUsername = null; @@ -154,7 +154,7 @@ public function testExceptionEncounteredWhileCreatingDatabaseAttemptsToCleanup() $server = $this->createServerModel(); $name = DatabaseManagementService::generateUniqueDatabaseName('something', $server->id); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); $this->repository->expects('createDatabase')->with($name)->andThrows(new \BadMethodCallException()); $this->repository->expects('dropDatabase')->with($name); diff --git a/tests/Integration/Services/Databases/DeployServerDatabaseServiceTest.php b/tests/Integration/Services/Databases/DeployServerDatabaseServiceTest.php index 062dbe07d0..60986f5766 100644 --- a/tests/Integration/Services/Databases/DeployServerDatabaseServiceTest.php +++ b/tests/Integration/Services/Databases/DeployServerDatabaseServiceTest.php @@ -62,7 +62,7 @@ public function testErrorIsThrownIfNoDatabaseHostsExistOnNode(): void $server = $this->createServerModel(); $node = Node::factory()->create(); - DatabaseHost::factory()->create(['node_id' => $node->id]); + DatabaseHost::factory()->recycle($node)->create(); config()->set('panel.client_features.databases.allow_random', false); @@ -95,10 +95,7 @@ public function testErrorIsThrownIfNoDatabaseHostsExistOnSystem(): void public function testDatabaseHostOnSameNodeIsPreferred(): void { $server = $this->createServerModel(); - - $node = Node::factory()->create(); - DatabaseHost::factory()->create(['node_id' => $node->id]); - $host = DatabaseHost::factory()->create(['node_id' => $server->node_id]); + $host = DatabaseHost::factory()->recycle($server->node)->create(); $this->managementService->expects('create')->with($server, [ 'database_host_id' => $host->id, @@ -124,7 +121,7 @@ public function testDatabaseHostIsSelectedIfNoSuitableHostExistsOnSameNode(): vo $server = $this->createServerModel(); $node = Node::factory()->create(); - $host = DatabaseHost::factory()->create(['node_id' => $node->id]); + $host = DatabaseHost::factory()->recycle($node)->create(); $this->managementService->expects('create')->with($server, [ 'database_host_id' => $host->id,