From d5cc2a2eedc644746049a04626ed886dbb4fcb5f Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 25 Oct 2023 09:28:26 +0200 Subject: [PATCH 1/4] feat: download local backups --- .../Project/Database/BackupExecutions.php | 24 +++++++ composer.json | 1 + composer.lock | 62 ++++++++++++++++++- config/sentry.php | 2 +- config/version.php | 2 +- .../database/backup-executions.blade.php | 8 +-- versions.json | 2 +- 7 files changed, 93 insertions(+), 8 deletions(-) diff --git a/app/Http/Livewire/Project/Database/BackupExecutions.php b/app/Http/Livewire/Project/Database/BackupExecutions.php index 2f808d992..f8ec4efbe 100644 --- a/app/Http/Livewire/Project/Database/BackupExecutions.php +++ b/app/Http/Livewire/Project/Database/BackupExecutions.php @@ -2,6 +2,7 @@ namespace App\Http\Livewire\Project\Database; +use Illuminate\Support\Facades\Storage; use Livewire\Component; class BackupExecutions extends Component @@ -23,6 +24,29 @@ class BackupExecutions extends Component $this->emit('success', 'Backup deleted successfully.'); $this->emit('refreshBackupExecutions'); } + public function download($exeuctionId) + { + try { + $execution = $this->backup->executions()->where('id', $exeuctionId)->first(); + if (is_null($execution)) { + $this->emit('error', 'Backup execution not found.'); + return; + } + $filename = data_get($execution, 'filename'); + $server = $execution->scheduledDatabaseBackup->database->destination->server; + $privateKeyLocation = savePrivateKeyToFs($server); + $disk = Storage::build([ + 'driver' => 'sftp', + 'host' => $server->ip, + 'port' => $server->port, + 'username' => $server->user, + 'privateKey' => $privateKeyLocation, + ]); + return $disk->download($filename); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } public function refreshBackupExecutions(): void { $this->executions = $this->backup->executions; diff --git a/composer.json b/composer.json index 217560b57..9937ee5b9 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "laravel/ui": "^4.2", "lcobucci/jwt": "^5.0.0", "league/flysystem-aws-s3-v3": "^3.0", + "league/flysystem-sftp-v3": "^3.0", "livewire/livewire": "^v2.12.3", "lorisleiva/laravel-actions": "^2.7", "masmerise/livewire-toaster": "^1.2", diff --git a/composer.lock b/composer.lock index 9f9e8d658..62d4f27ff 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "de2c45be3f03d43430549d963778dc4a", + "content-hash": "21ed976753483557403be75318585442", "packages": [ { "name": "aws/aws-crt-php", @@ -2938,6 +2938,66 @@ ], "time": "2023-08-30T10:23:59+00:00" }, + { + "name": "league/flysystem-sftp-v3", + "version": "3.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-sftp-v3.git", + "reference": "1ba682def8e87fd7fa00883629553c0200d2e974" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-sftp-v3/zipball/1ba682def8e87fd7fa00883629553c0200d2e974", + "reference": "1ba682def8e87fd7fa00883629553c0200d2e974", + "shasum": "" + }, + "require": { + "league/flysystem": "^3.0.14", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2", + "phpseclib/phpseclib": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\PhpseclibV3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "SFTP filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "sftp" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-sftp-v3/issues", + "source": "https://github.com/thephpleague/flysystem-sftp-v3/tree/3.16.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2023-08-30T10:25:05+00:00" + }, { "name": "league/mime-type-detection", "version": "1.13.0", diff --git a/config/sentry.php b/config/sentry.php index 32e27e081..16f184229 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.101', + 'release' => '4.0.0-beta.102', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index d54064579..004c77c3b 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ Location: {{ data_get($execution, 'filename', 'N/A') }}
- - {{-- @if (data_get($execution, 'status') !== 'failed') --}} - {{-- Download --}} - {{-- @endif --}} + @if (data_get($execution, 'status') === 'success') + Download + @endif Delete
diff --git a/versions.json b/versions.json index 4c8e06b9d..cd5da1110 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.101" + "version": "4.0.0-beta.102" } } } From 70ecb92e82c69aecbe912ef48d1aa7e3dea83884 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 25 Oct 2023 09:41:41 +0200 Subject: [PATCH 2/4] cleanup ssh dir on start --- app/Console/Commands/Init.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 6abac7029..31d7639be 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -10,6 +10,7 @@ use App\Models\StandaloneMongodb; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Storage; class Init extends Command { @@ -21,8 +22,23 @@ class Init extends Command ray()->clearAll(); $this->cleanup_in_progress_application_deployments(); $this->cleanup_stucked_resources(); + $this->cleanup_ssh(); } + private function cleanup_ssh() { + try { + $files = Storage::allFiles('ssh/keys'); + foreach ($files as $file) { + Storage::delete($file); + } + $files = Storage::allFiles('ssh/mux'); + foreach ($files as $file) { + Storage::delete($file); + } + } catch (\Throwable $e) { + echo "Error: {$e->getMessage()}\n"; + } + } private function cleanup_in_progress_application_deployments() { // Cleanup any failed deployments From aa02b8d4332a671bd2c430f11b85fa53254b84e7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 25 Oct 2023 09:56:58 +0200 Subject: [PATCH 3/4] fix: rate limit for api + add mariadb + mysql --- app/Providers/RouteServiceProvider.php | 2 +- routes/api.php | 34 +++++++++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index f60994c61..79b214502 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -48,7 +48,7 @@ class RouteServiceProvider extends ServiceProvider if ($request->path() === 'api/health') { return Limit::perMinute(1000)->by($request->user()?->id ?: $request->ip()); } - return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip()); + return Limit::perMinute(200)->by($request->user()?->id ?: $request->ip()); }); RateLimiter::for('5', function (Request $request) { return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip()); diff --git a/routes/api.php b/routes/api.php index 77c000576..53ae1bd09 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,8 @@ query->get('uuid'); $force = $request->query->get('force') ?? false; - if (is_null($teamId)) { return response()->json(['error' => 'Invalid token.'], 400); } @@ -50,29 +51,56 @@ Route::group([ ); return response()->json(['message' => 'Deployment queued.'], 200); } else if ($type === 'App\Models\StandalonePostgresql') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } StartPostgresql::run($resource); $resource->update([ 'started_at' => now(), ]); return response()->json(['message' => 'Database started.'], 200); } else if ($type === 'App\Models\StandaloneRedis') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } StartRedis::run($resource); $resource->update([ 'started_at' => now(), ]); return response()->json(['message' => 'Database started.'], 200); } else if ($type === 'App\Models\StandaloneMongodb') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } StartMongodb::run($resource); $resource->update([ 'started_at' => now(), ]); return response()->json(['message' => 'Database started.'], 200); - }else if ($type === 'App\Models\Service') { + } else if ($type === 'App\Models\StandaloneMysql') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } + StartMysql::run($resource); + $resource->update([ + 'started_at' => now(), + ]); + return response()->json(['message' => 'Database started.'], 200); + } else if ($type === 'App\Models\StandaloneMariadb') { + if (str($resource->status)->startsWith('running')) { + return response()->json(['message' => 'Database already running.'], 200); + } + StartMariadb::run($resource); + $resource->update([ + 'started_at' => now(), + ]); + return response()->json(['message' => 'Database started.'], 200); + } else if ($type === 'App\Models\Service') { StartService::run($resource); return response()->json(['message' => 'Service started.'], 200); } } - return response()->json(['error' => 'No resource found.'], 404); + return response()->json(['error' => "No resource found with {$uuid}."], 404); }); }); From 379f4b9dff1310e165844fa356f989b293286d24 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Wed, 25 Oct 2023 10:43:07 +0200 Subject: [PATCH 4/4] feat: show webhook on ui feat: n8n service --- app/Http/Livewire/Project/Shared/Webhooks.php | 19 ++++++++++ bootstrap/helpers/shared.php | 15 ++++++++ .../livewire/project/service/index.blade.php | 25 ++++++++---- .../project/shared/webhooks.blade.php | 10 +++++ .../application/configuration.blade.php | 6 +++ .../project/database/configuration.blade.php | 6 +++ routes/api.php | 2 +- templates/compose/appsmith.yaml | 2 + templates/compose/n8n-with-postgresql.yaml | 38 +++++++++++++++++++ templates/compose/n8n.yaml | 15 ++++++++ templates/service-templates.json | 30 ++++++++++++++- 11 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 app/Http/Livewire/Project/Shared/Webhooks.php create mode 100644 resources/views/livewire/project/shared/webhooks.blade.php create mode 100644 templates/compose/n8n-with-postgresql.yaml create mode 100644 templates/compose/n8n.yaml diff --git a/app/Http/Livewire/Project/Shared/Webhooks.php b/app/Http/Livewire/Project/Shared/Webhooks.php new file mode 100644 index 000000000..a943347b1 --- /dev/null +++ b/app/Http/Livewire/Project/Shared/Webhooks.php @@ -0,0 +1,19 @@ +deploywebhook = generateDeployWebhook($this->resource); + } + public function render() + { + return view('livewire.project.shared.webhooks'); + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 3348ce1ae..8e8b2ec3e 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -4,7 +4,9 @@ use App\Models\Application; use App\Models\InstanceSettings; use App\Models\Server; use App\Models\Service; +use App\Models\StandaloneMariadb; use App\Models\StandaloneMongodb; +use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; use App\Models\Team; @@ -484,5 +486,18 @@ function queryResourcesByUuid(string $uuid) if ($redis) return $redis; $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); if ($mongodb) return $mongodb; + $mysql = StandaloneMysql::whereUuid($uuid)->first(); + if ($mysql) return $mysql; + $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); + if ($mariadb) return $mariadb; return $resource; } + +function generateDeployWebhook($resource) { + $baseUrl = base_url(); + $api = Url::fromString($baseUrl) . '/api/v1'; + $endpoint = '/deploy'; + $uuid = data_get($resource, 'uuid'); + $url = $api . $endpoint . "?uuid=$uuid&force=false"; + return $url; +} diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index a966c56d4..933c89ecd 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -4,16 +4,25 @@
@@ -100,7 +109,9 @@ @foreach ($databases as $database) @endforeach - +
+
+
diff --git a/resources/views/livewire/project/shared/webhooks.blade.php b/resources/views/livewire/project/shared/webhooks.blade.php new file mode 100644 index 000000000..ba0461bdb --- /dev/null +++ b/resources/views/livewire/project/shared/webhooks.blade.php @@ -0,0 +1,10 @@ +
+
+

Webhooks

+ +
+
+ +
+
diff --git a/resources/views/project/application/configuration.blade.php b/resources/views/project/application/configuration.blade.php index 78f0a3061..d4766362f 100644 --- a/resources/views/project/application/configuration.blade.php +++ b/resources/views/project/application/configuration.blade.php @@ -19,6 +19,9 @@ Storages + Webhooks + @if ($application->git_based()) Preview @@ -57,6 +60,9 @@
+
+ +
diff --git a/resources/views/project/database/configuration.blade.php b/resources/views/project/database/configuration.blade.php index 3ad6e6df5..c65ef4301 100644 --- a/resources/views/project/database/configuration.blade.php +++ b/resources/views/project/database/configuration.blade.php @@ -31,6 +31,9 @@ window.location.hash = 'storages'" href="#">Storages
+ Webhooks +
+
+ +
diff --git a/routes/api.php b/routes/api.php index 53ae1bd09..ef233e37e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -97,7 +97,7 @@ Route::group([ return response()->json(['message' => 'Database started.'], 200); } else if ($type === 'App\Models\Service') { StartService::run($resource); - return response()->json(['message' => 'Service started.'], 200); + return response()->json(['message' => 'Service started. It could take a while, be patient.'], 200); } } return response()->json(['error' => "No resource found with {$uuid}."], 404); diff --git a/templates/compose/appsmith.yaml b/templates/compose/appsmith.yaml index a30f4c4e5..81ac5fe3d 100644 --- a/templates/compose/appsmith.yaml +++ b/templates/compose/appsmith.yaml @@ -14,3 +14,5 @@ services: - APPSMITH_SMART_LOOK_ID= volumes: - stacks-data:/appsmith-stacks + healthcheck: + test: ["NONE"] diff --git a/templates/compose/n8n-with-postgresql.yaml b/templates/compose/n8n-with-postgresql.yaml new file mode 100644 index 000000000..25955583d --- /dev/null +++ b/templates/compose/n8n-with-postgresql.yaml @@ -0,0 +1,38 @@ +# documentation: https://docs.n8n.io/hosting/ +# slogan: n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model. +# tags: n8n,workflow,automation,open,source,low,code + +services: + n8n: + image: docker.n8n.io/n8nio/n8n + environment: + - SERVICE_FQDN_N8N + - N8N_EDITOR_BASE_URL=${SERVICE_FQDN_N8N} + - N8N_HOST=${SERVICE_FQDN_N8N} + - GENERIC_TIMEZONE="Europe/Berlin" + - TZ="Europe/Berlin" + - DB_TYPE=postgresdb + - DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-umami} + - DB_POSTGRESDB_HOST=postgresql + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_USER=$SERVICE_USER_POSTGRES + - DB_POSTGRESDB_SCHEMA=public + - DB_POSTGRESDB_PASSWORD=$SERVICE_PASSWORD_POSTGRES + volumes: + - n8n-data:/home/node/.n8n + depends_on: + - postgresql + postgresql: + image: postgres:15-alpine + volumes: + - postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-umami} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 10 + diff --git a/templates/compose/n8n.yaml b/templates/compose/n8n.yaml new file mode 100644 index 000000000..c8613cf03 --- /dev/null +++ b/templates/compose/n8n.yaml @@ -0,0 +1,15 @@ +# documentation: https://docs.n8n.io/hosting/ +# slogan: n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model. +# tags: n8n,workflow,automation,open,source,low,code + +services: + n8n: + image: docker.n8n.io/n8nio/n8n + environment: + - SERVICE_FQDN_N8N + - N8N_EDITOR_BASE_URL=${SERVICE_FQDN_N8N} + - N8N_HOST=${SERVICE_FQDN_N8N} + - GENERIC_TIMEZONE="Europe/Berlin" + - TZ="Europe/Berlin" + volumes: + - n8n-data:/home/node/.n8n diff --git a/templates/service-templates.json b/templates/service-templates.json index d5af2b37c..4c06f76c0 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -2,7 +2,7 @@ "appsmith": { "documentation": "https:\/\/docs.appsmith.com", "slogan": "Appsmith is an open-source, self-hosted application development platform that enables you to build powerful web applications with ease.", - "compose": "c2VydmljZXM6CiAgYXBwc21pdGg6CiAgICBpbWFnZTogJ2luZGV4LmRvY2tlci5pby9hcHBzbWl0aC9hcHBzbWl0aC1jZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE4KICAgICAgLSBBUFBTTUlUSF9NQUlMX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSBBUFBTTUlUSF9ESVNBQkxFX1RFTEVNRVRSWT10cnVlCiAgICAgIC0gQVBQU01JVEhfRElTQUJMRV9JTlRFUkNPTT10cnVlCiAgICAgIC0gQVBQU01JVEhfU0VOVFJZX0RTTj0KICAgICAgLSBBUFBTTUlUSF9TTUFSVF9MT09LX0lEPQogICAgdm9sdW1lczoKICAgICAgLSAnc3RhY2tzLWRhdGE6L2FwcHNtaXRoLXN0YWNrcycK", + "compose": "c2VydmljZXM6CiAgYXBwc21pdGg6CiAgICBpbWFnZTogJ2luZGV4LmRvY2tlci5pby9hcHBzbWl0aC9hcHBzbWl0aC1jZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE4KICAgICAgLSBBUFBTTUlUSF9NQUlMX0VOQUJMRUQ9ZmFsc2UKICAgICAgLSBBUFBTTUlUSF9ESVNBQkxFX1RFTEVNRVRSWT10cnVlCiAgICAgIC0gQVBQU01JVEhfRElTQUJMRV9JTlRFUkNPTT10cnVlCiAgICAgIC0gQVBQU01JVEhfU0VOVFJZX0RTTj0KICAgICAgLSBBUFBTTUlUSF9TTUFSVF9MT09LX0lEPQogICAgdm9sdW1lczoKICAgICAgLSAnc3RhY2tzLWRhdGE6L2FwcHNtaXRoLXN0YWNrcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gTk9ORQo=", "tags": [ "lowcode", "nocode", @@ -110,6 +110,34 @@ "api" ] }, + "n8n-with-postgresql": { + "documentation": "https:\/\/docs.n8n.io\/hosting\/", + "slogan": "n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model.", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOCiAgICAgIC0gJ044Tl9FRElUT1JfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0iRXVyb3BlL0JlcmxpbiInCiAgICAgIC0gJ1RaPSJFdXJvcGUvQmVybGluIicKICAgICAgLSBEQl9UWVBFPXBvc3RncmVzZGIKICAgICAgLSAnREJfUE9TVEdSRVNEQl9EQVRBQkFTRT0ke1BPU1RHUkVTX0RCOi11bWFtaX0nCiAgICAgIC0gREJfUE9TVEdSRVNEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1NUR1JFU0RCX1BPUlQ9NTQzMgogICAgICAtIERCX1BPU1RHUkVTREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gREJfUE9TVEdSRVNEQl9TQ0hFTUE9cHVibGljCiAgICAgIC0gREJfUE9TVEdSRVNEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgdm9sdW1lczoKICAgICAgLSAnbjhuLWRhdGE6L2hvbWUvbm9kZS8ubjhuJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3Jlc3FsCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXVtYW1pfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "n8n", + "workflow", + "automation", + "open", + "source", + "low", + "code" + ] + }, + "n8n": { + "documentation": "https:\/\/docs.n8n.io\/hosting\/", + "slogan": "n8n is an extendable workflow automation tool which enables you to connect anything to everything via its open, fair-code model.", + "compose": "c2VydmljZXM6CiAgbjhuOgogICAgaW1hZ2U6IGRvY2tlci5uOG4uaW8vbjhuaW8vbjhuCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTjhOCiAgICAgIC0gJ044Tl9FRElUT1JfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnTjhOX0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTjhOfScKICAgICAgLSAnR0VORVJJQ19USU1FWk9ORT0iRXVyb3BlL0JlcmxpbiInCiAgICAgIC0gJ1RaPSJFdXJvcGUvQmVybGluIicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ244bi1kYXRhOi9ob21lL25vZGUvLm44bicK", + "tags": [ + "n8n", + "workflow", + "automation", + "open", + "source", + "low", + "code" + ] + }, "pairdrop": { "documentation": "https:\/\/github.com\/schlagmichdoch\/PairDrop\/blob\/master\/docs\/faq.md", "slogan": "Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.",