diff --git a/README.md b/README.md index 56bee004e..e45b60b83 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,21 @@ +![Latest Release Version](https://img.shields.io/badge/dynamic/json?labelColor=grey&color=6366f1&label=Latest_released_version&url=https%3A%2F%2Fcdn.coollabs.io%2Fcoolify%2Fversions.json&query=coolify.v4.version&style=for-the-badge +) + [![Bounty Issues](https://img.shields.io/static/v1?labelColor=grey&color=6366f1&label=Algora&message=%F0%9F%92%8E+Bounty+issues&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties/new) [![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dopen&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=open) [![Rewarded Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dcompleted&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=completed) + # About the Project Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Vercel / etc. -It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything. +It helps you manage your servers, applications, and databases on your own hardware; you only need an SSH connection. You can manage VPS, Bare Metal, Raspberry PIs, and anything else. -Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**. +Imagine having the ease of a cloud but with your own servers. That is **Coolify**. -No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️ +No vendor lock-in, which means that all the configurations for your applications/databases/etc are saved to your server. So, if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You lose the automations and all the magic. 🪄️ -For more information, take a look at our landing page [here](https://coolify.io). +For more information, take a look at our landing page at [coolify.io](https://coolify.io). # Installation @@ -22,36 +26,42 @@ # Installation # Support -Contact us [here](https://coolify.io/docs/contact). +Contact us at [coolify.io/docs/contact](https://coolify.io/docs/contact). # Donations -To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project. +To stay completely free and open-source, with no feature behind the paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the project's future development. -https://coolify.io/sponsorships +[coolify.io/sponsorships](https://coolify.io/sponsorships) Thank you so much! Special thanks to our biggest sponsors! - - - - - - + + + + + + + + + + + ## Github Sponsors ($40+) - - - + + + - - - - - - + + + + + + + @@ -83,9 +93,9 @@ ## Individuals # Cloud -If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io +If you do not want to self-host Coolify, there is a paid cloud version available: [app.coolify.io](https://app.coolify.io) -For more information & pricing, take a look at our landing page [here](https://coolify.io). +For more information & pricing, take a look at our landing page [coolify.io](https://coolify.io). ## Why should I use the Cloud version? The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month. @@ -109,7 +119,7 @@ # Recognitions
- + diff --git a/app/Actions/Application/LoadComposeFile.php b/app/Actions/Application/LoadComposeFile.php new file mode 100644 index 000000000..838b541e2 --- /dev/null +++ b/app/Actions/Application/LoadComposeFile.php @@ -0,0 +1,16 @@ +loadComposeFile(); + } +} diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php index d4cdf64e2..686b60780 100644 --- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php +++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php @@ -3,6 +3,7 @@ namespace App\Actions\CoolifyTask; use App\Data\CoolifyTaskArgs; +use App\Enums\ActivityTypes; use App\Jobs\CoolifyTask; use Spatie\Activitylog\Models\Activity; @@ -40,8 +41,18 @@ public function __construct(CoolifyTaskArgs $remoteProcessArgs) public function __invoke(): Activity { - $job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_data: $this->remoteProcessArgs->call_event_data); - dispatch($job); + $job = new CoolifyTask( + activity: $this->activity, + ignore_errors: $this->remoteProcessArgs->ignore_errors, + call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, + call_event_data: $this->remoteProcessArgs->call_event_data, + ); + if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) { + ray('Dispatching a high priority job'); + dispatch($job)->onQueue('high'); + } else { + dispatch($job); + } $this->activity->refresh(); return $this->activity; diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index be986a76f..63e3afe2f 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -39,7 +39,7 @@ class RunRemoteProcess public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null) { - if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) { + if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value && $activity->getExtraProperty('type') !== ActivityTypes::COMMAND->value) { throw new \RuntimeException('Incompatible Activity to run a remote command.'); } diff --git a/app/Actions/Database/RestartDatabase.php b/app/Actions/Database/RestartDatabase.php new file mode 100644 index 000000000..0400d924d --- /dev/null +++ b/app/Actions/Database/RestartDatabase.php @@ -0,0 +1,29 @@ +destination->server; + if (! $server->isFunctional()) { + return 'Server is not functional'; + } + StopDatabase::run($database); + + return StartDatabase::run($database); + } +} diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index d9518cd80..e97c55930 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneClickhouse; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -155,11 +154,11 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) { $environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) { $environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}"); } diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php new file mode 100644 index 000000000..323c52ff9 --- /dev/null +++ b/app/Actions/Database/StartDatabase.php @@ -0,0 +1,57 @@ +destination->server; + if (! $server->isFunctional()) { + return 'Server is not functional'; + } + switch ($database->getMorphClass()) { + case 'App\Models\StandalonePostgresql': + $activity = StartPostgresql::run($database); + break; + case 'App\Models\StandaloneRedis': + $activity = StartRedis::run($database); + break; + case 'App\Models\StandaloneMongodb': + $activity = StartMongodb::run($database); + break; + case 'App\Models\StandaloneMysql': + $activity = StartMysql::run($database); + break; + case 'App\Models\StandaloneMariadb': + $activity = StartMariadb::run($database); + break; + case 'App\Models\StandaloneKeydb': + $activity = StartKeydb::run($database); + break; + case 'App\Models\StandaloneDragonfly': + $activity = StartDragonfly::run($database); + break; + case 'App\Models\StandaloneClickhouse': + $activity = StartClickhouse::run($database); + break; + } + if ($database->is_public && $database->public_port) { + StartDatabaseProxy::dispatch($database); + } + + return $activity; + } +} diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 19b1c5814..862fc54fc 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneDragonfly; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -155,7 +154,7 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { $environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}"); } diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index a632f6e8c..85cb89c1c 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -4,7 +4,6 @@ use App\Models\StandaloneKeydb; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -163,7 +162,7 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { $environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}"); } diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index 31d3f0640..33948192b 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneMariadb; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -157,18 +156,18 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) { $environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) { $environment_variables->push("MARIADB_USER={$this->database->mariadb_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MARIADB_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) { $environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}"); } diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 8db34b20f..911054f5e 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneMongodb; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -174,15 +173,15 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) { $environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}"); } diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 8280faa56..b55d9dead 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandaloneMysql; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -157,18 +156,18 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) { $environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_DATABASE'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) { $environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) { $environment_variables->push("MYSQL_USER={$this->database->mysql_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MYSQL_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) { $environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}"); } diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 23b9742c7..909f4c893 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -3,7 +3,6 @@ namespace App\Actions\Database; use App\Models\StandalonePostgresql; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -179,18 +178,18 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_USER'))->isEmpty()) { $environment_variables->push("POSTGRES_USER={$this->database->postgres_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('PGUSER'))->isEmpty()) { $environment_variables->push("PGUSER={$this->database->postgres_user}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) { $environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_DB'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_DB'))->isEmpty()) { $environment_variables->push("POSTGRES_DB={$this->database->postgres_db}"); } diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index 065df5e52..f10afef5e 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -4,7 +4,6 @@ use App\Models\StandaloneRedis; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -167,7 +166,7 @@ private function generate_environment_variables() $environment_variables->push("$env->key=$env->real_value"); } - if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) { + if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { $environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}"); } diff --git a/app/Actions/Database/StopDatabase.php b/app/Actions/Database/StopDatabase.php index 66a32e811..e4903ff35 100644 --- a/app/Actions/Database/StopDatabase.php +++ b/app/Actions/Database/StopDatabase.php @@ -29,7 +29,5 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St if ($database->is_public) { StopDatabaseProxy::run($database); } - // TODO: make notification for services - // $database->environment->project->team->notify(new StatusChanged($database)); } } diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index 984225435..b2092e2ef 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -2,6 +2,7 @@ namespace App\Actions\Database; +use App\Events\DatabaseStatusChanged; use App\Models\ServiceDatabase; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -26,7 +27,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $server = data_get($database, 'service.server'); } instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); - $database->is_public = false; $database->save(); + DatabaseStatusChanged::dispatch(); } } diff --git a/app/Actions/Proxy/CheckConfiguration.php b/app/Actions/Proxy/CheckConfiguration.php index 35374ba43..3cbd5669d 100644 --- a/app/Actions/Proxy/CheckConfiguration.php +++ b/app/Actions/Proxy/CheckConfiguration.php @@ -3,7 +3,6 @@ namespace App\Actions\Proxy; use App\Models\Server; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; class CheckConfiguration @@ -24,7 +23,7 @@ public function handle(Server $server, bool $reset = false) $proxy_configuration = instant_remote_process($payload, $server, false); if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) { - $proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value; + $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value; } if (! $proxy_configuration || is_null($proxy_configuration)) { throw new \Exception('Could not generate proxy configuration'); diff --git a/app/Actions/Proxy/SaveConfiguration.php b/app/Actions/Proxy/SaveConfiguration.php index 4c413ca36..f2de2b3f5 100644 --- a/app/Actions/Proxy/SaveConfiguration.php +++ b/app/Actions/Proxy/SaveConfiguration.php @@ -3,7 +3,6 @@ namespace App\Actions\Proxy; use App\Models\Server; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; class SaveConfiguration @@ -18,7 +17,7 @@ public function handle(Server $server, ?string $proxy_settings = null) $proxy_path = $server->proxyPath(); $docker_compose_yml_base64 = base64_encode($proxy_settings); - $server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; + $server->proxy->last_saved_settings = str($docker_compose_yml_base64)->pipe('md5')->value; $server->save(); return instant_remote_process([ diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index 710b5cdd8..341186bec 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -4,7 +4,6 @@ use App\Events\ProxyStarted; use App\Models\Server; -use Illuminate\Support\Str; use Lorisleiva\Actions\Concerns\AsAction; use Spatie\Activitylog\Models\Activity; @@ -27,7 +26,7 @@ public function handle(Server $server, bool $async = true): string|Activity } SaveConfiguration::run($server, $configuration); $docker_compose_yml_base64 = base64_encode($configuration); - $server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value; + $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value; $server->save(); if ($server->isSwarm()) { $commands = $commands->merge([ diff --git a/app/Actions/Server/RunCommand.php b/app/Actions/Server/RunCommand.php new file mode 100644 index 000000000..fce862eb0 --- /dev/null +++ b/app/Actions/Server/RunCommand.php @@ -0,0 +1,19 @@ +value); + + return $activity; + } +} diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index eea429c79..b79bc8f67 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -12,12 +12,15 @@ class StartSentinel public function handle(Server $server, $version = 'latest', bool $restart = false) { if ($restart) { - instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); + StopSentinel::run($server); } + $metrics_history = $server->settings->metrics_history_days; + $refresh_rate = $server->settings->metrics_refresh_rate_seconds; + $token = $server->settings->metrics_token; instant_remote_process([ - "docker run --rm --pull always -d -e \"SCHEDULER=true\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", + "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', 'chmod -R 700 /data/coolify/metrics /data/coolify/logs', - ], $server, false); + ], $server, true); } } diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php new file mode 100644 index 000000000..21ffca3bd --- /dev/null +++ b/app/Actions/Server/StopSentinel.php @@ -0,0 +1,16 @@ +server) { return; } - CleanupDocker::run($this->server, false); + CleanupDocker::dispatch($this->server, false)->onQueue('high'); $this->latestVersion = get_latest_version_of_coolify(); $this->currentVersion = config('version'); if (! $manual_update) { @@ -48,6 +48,7 @@ public function handle($manual_update = false) private function update() { if (isDev()) { + ray('Running in dev mode'); remote_process([ 'sleep 10', ], $this->server); diff --git a/app/Actions/Service/RestartService.php b/app/Actions/Service/RestartService.php new file mode 100644 index 000000000..1b6a5c32c --- /dev/null +++ b/app/Actions/Service/RestartService.php @@ -0,0 +1,18 @@ +error('This command can only be run on cloud'); + + return; + } + ray()->clearAll(); + $this->info('Cleaning up subcriptions teams'); + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + + $teams = Team::all()->filter(function ($team) { + return $team->id !== 0; + })->sortBy('id'); + foreach ($teams as $team) { + if ($team) { + $this->info("Checking team {$team->id}"); + } + if (! data_get($team, 'subscription')) { + $this->disableServers($team); + + continue; + } + // If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status + if (! (data_get($team, 'subscription.stripe_subscription_id'))) { + $this->info("Resetting invoice paid status for team {$team->id} {$team->name}"); + + $team->subscription->update([ + 'stripe_invoice_paid' => false, + 'stripe_trial_already_ended' => false, + 'stripe_subscription_id' => null, + ]); + $this->disableServers($team); + + continue; + } else { + $subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []); + $status = data_get($subscription, 'status'); + if ($status === 'active' || $status === 'past_due') { + $team->subscription->update([ + 'stripe_invoice_paid' => true, + 'stripe_trial_already_ended' => false, + ]); + + continue; + } + $this->info('Subscription status: '.$status); + $this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id')); + $confirm = $this->confirm('Do you want to cancel the subscription?', true); + if (! $confirm) { + $this->info("Skipping team {$team->id} {$team->name}"); + } else { + $this->info("Cancelling subscription for team {$team->id} {$team->name}"); + $team->subscription->update([ + 'stripe_invoice_paid' => false, + 'stripe_trial_already_ended' => false, + 'stripe_subscription_id' => null, + ]); + $this->disableServers($team); + } + } + } + + } catch (\Exception $e) { + $this->error($e->getMessage()); + + return; + } + } + + private function disableServers(Team $team) + { + foreach ($team->servers as $server) { + if ($server->settings->is_usable === true || $server->settings->is_reachable === true || $server->ip !== '1.2.3.4') { + $this->info("Disabling server {$server->id} {$server->name}"); + $server->settings()->update([ + 'is_usable' => false, + 'is_reachable' => false, + ]); + $server->update([ + 'ip' => '1.2.3.4', + ]); + } + } + + } +} diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php index 80059bf00..964b8e46e 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -9,13 +9,41 @@ class Dev extends Command { - protected $signature = 'dev:init'; + protected $signature = 'dev {--init} {--generate-openapi}'; - protected $description = 'Init the app in dev mode'; + protected $description = 'Helper commands for development.'; public function handle() + { + if ($this->option('init')) { + $this->init(); + + return; + } + if ($this->option('generate-openapi')) { + $this->generateOpenApi(); + + return; + } + + } + + public function generateOpenApi() + { + // Generate OpenAPI documentation + echo "Generating OpenAPI documentation.\n"; + $process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']); + $error = $process->errorOutput(); + $error = preg_replace('/^.*an object literal,.*$/m', '', $error); + $error = preg_replace('/^\h*\v+/m', '', $error); + echo $error; + echo $process->output(); + } + + public function init() { // Generate APP_KEY if not exists + if (empty(env('APP_KEY'))) { echo "Generating APP_KEY.\n"; Artisan::call('key:generate'); diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index 50c9fe29b..7949b2457 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -5,6 +5,7 @@ use App\Enums\ApplicationDeploymentStatus; use App\Jobs\CleanupHelperContainersJob; use App\Models\ApplicationDeploymentQueue; +use App\Models\Environment; use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; @@ -24,6 +25,8 @@ public function handle() get_public_ips(); $full_cleanup = $this->option('full-cleanup'); $cleanup_deployments = $this->option('cleanup-deployments'); + + $this->replace_slash_in_environment_name(); if ($cleanup_deployments) { echo "Running cleanup deployments.\n"; $this->cleanup_in_progress_application_deployments(); @@ -150,4 +153,15 @@ private function cleanup_in_progress_application_deployments() echo "Error: {$e->getMessage()}\n"; } } + + private function replace_slash_in_environment_name() + { + $environments = Environment::all(); + foreach ($environments as $environment) { + if (str_contains($environment->name, '/')) { + $environment->name = str_replace('/', '-', $environment->name); + $environment->save(); + } + } + } } diff --git a/app/Console/Commands/WaitlistInvite.php b/app/Console/Commands/WaitlistInvite.php index 88ff21d46..ff501cc1d 100644 --- a/app/Console/Commands/WaitlistInvite.php +++ b/app/Console/Commands/WaitlistInvite.php @@ -82,7 +82,7 @@ private function register_user() if (! $already_registered) { $this->password = Str::password(); User::create([ - 'name' => Str::of($this->next_patient->email)->before('@'), + 'name' => str($this->next_patient->email)->before('@'), 'email' => $this->next_patient->email, 'password' => Hash::make($this->password), 'force_password_reset' => true, diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e2698d90e..f529f63b9 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -61,7 +61,7 @@ private function pull_images($schedule) { $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); foreach ($servers as $server) { - if (config('coolify.is_sentinel_enabled')) { + if ($server->isSentinelEnabled()) { $schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer(); } $schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer(); diff --git a/app/Data/ServerMetadata.php b/app/Data/ServerMetadata.php index b96efa622..d95944b15 100644 --- a/app/Data/ServerMetadata.php +++ b/app/Data/ServerMetadata.php @@ -11,6 +11,5 @@ class ServerMetadata extends Data public function __construct( public ?ProxyTypes $type, public ?ProxyStatus $status - ) { - } + ) {} } diff --git a/app/Enums/ActivityTypes.php b/app/Enums/ActivityTypes.php index e2536a7f0..2d23cd98b 100644 --- a/app/Enums/ActivityTypes.php +++ b/app/Enums/ActivityTypes.php @@ -5,4 +5,5 @@ enum ActivityTypes: string { case INLINE = 'inline'; + case COMMAND = 'command'; } diff --git a/app/Enums/BuildPackTypes.php b/app/Enums/BuildPackTypes.php new file mode 100644 index 000000000..cb51db6d6 --- /dev/null +++ b/app/Enums/BuildPackTypes.php @@ -0,0 +1,11 @@ +user()->id ?? null; } if (is_null($userId)) { - throw new \Exception('User id is null'); + return false; } $this->userId = $userId; } - public function broadcastOn(): array + public function broadcastOn(): ?array { - return [ - new PrivateChannel("user.{$this->userId}"), - ]; + if ($this->userId) { + return [ + new PrivateChannel("user.{$this->userId}"), + ]; + } + + return null; } } diff --git a/app/Events/ProxyStarted.php b/app/Events/ProxyStarted.php index ed62eccb1..64d562e0a 100644 --- a/app/Events/ProxyStarted.php +++ b/app/Events/ProxyStarted.php @@ -10,8 +10,5 @@ class ProxyStarted { use Dispatchable, InteractsWithSockets, SerializesModels; - public function __construct(public $data) - { - - } + public function __construct(public $data) {} } diff --git a/app/Events/ServiceStatusChanged.php b/app/Events/ServiceStatusChanged.php index e3e24a248..dc965d0a2 100644 --- a/app/Events/ServiceStatusChanged.php +++ b/app/Events/ServiceStatusChanged.php @@ -12,7 +12,7 @@ class ServiceStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public $userId; + public ?string $userId = null; public function __construct($userId = null) { @@ -20,15 +20,19 @@ public function __construct($userId = null) $userId = auth()->user()->id ?? null; } if (is_null($userId)) { - throw new \Exception('User id is null'); + return false; } $this->userId = $userId; } - public function broadcastOn(): array + public function broadcastOn(): ?array { - return [ - new PrivateChannel("user.{$this->userId}"), - ]; + if ($this->userId) { + return [ + new PrivateChannel("user.{$this->userId}"), + ]; + } + + return null; } } diff --git a/app/Exceptions/ProcessException.php b/app/Exceptions/ProcessException.php index 728a0d81b..47eaa6fd8 100644 --- a/app/Exceptions/ProcessException.php +++ b/app/Exceptions/ProcessException.php @@ -4,6 +4,4 @@ use Exception; -class ProcessException extends Exception -{ -} +class ProcessException extends Exception {} diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php new file mode 100644 index 000000000..c671ec44d --- /dev/null +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -0,0 +1,2539 @@ +user()->currentAccessToken(); + $application->makeHidden([ + 'id', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($application); + } + $application->makeHidden([ + 'custom_labels', + 'dockerfile', + 'docker_compose', + 'docker_compose_raw', + 'manual_webhook_secret_bitbucket', + 'manual_webhook_secret_gitea', + 'manual_webhook_secret_github', + 'manual_webhook_secret_gitlab', + 'private_key_id', + 'value', + 'real_value', + ]); + + return serializeApiResponse($application); + } + + #[OA\Get( + summary: 'List', + description: 'List all applications.', + path: '/applications', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all applications.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Application') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function applications(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::where('team_id', $teamId)->get(); + $applications = collect(); + $applications->push($projects->pluck('applications')->flatten()); + $applications = $applications->flatten(); + $applications = $applications->map(function ($application) { + return $this->removeSensitiveData($application); + }); + + return response()->json($applications); + } + + #[OA\Post( + summary: 'Create (Public)', + description: 'Create new application based on a public git repository.', + path: '/applications/public', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'install_command' => ['type' => 'string', 'description' => 'The install command.'], + 'build_command' => ['type' => 'string', 'description' => 'The build command.'], + 'start_command' => ['type' => 'string', 'description' => 'The start command.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + // 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], + 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_public_application(Request $request) + { + return $this->create_application($request, 'public'); + } + + #[OA\Post( + summary: 'Create (Private - GH App)', + description: 'Create new application based on a private repository through a Github App.', + path: '/applications/private-gh-app', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'github_app_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'], + 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'install_command' => ['type' => 'string', 'description' => 'The install command.'], + 'build_command' => ['type' => 'string', 'description' => 'The build command.'], + 'start_command' => ['type' => 'string', 'description' => 'The start command.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], + 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_private_gh_app_application(Request $request) + { + return $this->create_application($request, 'private-gh-app'); + } + + #[OA\Post( + summary: 'Create (Private - Deploy Key)', + description: 'Create new application based on a private repository through a Deploy Key.', + path: '/applications/private-deploy-key', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'private_key_uuid', 'git_repository', 'git_branch', 'build_pack', 'ports_exposes'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'private_key_uuid' => ['type' => 'string', 'description' => 'The private key UUID.'], + 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'install_command' => ['type' => 'string', 'description' => 'The install command.'], + 'build_command' => ['type' => 'string', 'description' => 'The build command.'], + 'start_command' => ['type' => 'string', 'description' => 'The start command.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], + 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_private_deploy_key_application(Request $request) + { + return $this->create_application($request, 'private-deploy-key'); + } + + #[OA\Post( + summary: 'Create (Dockerfile)', + description: 'Create new application based on a simple Dockerfile.', + path: '/applications/dockerfile', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'dockerfile'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_dockerfile_application(Request $request) + { + return $this->create_application($request, 'dockerfile'); + } + + #[OA\Post( + summary: 'Create (Docker Image)', + description: 'Create new application based on a prebuilt docker image', + path: '/applications/dockerimage', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_registry_image_name', 'ports_exposes'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_dockerimage_application(Request $request) + { + return $this->create_application($request, 'dockerimage'); + } + + #[OA\Post( + summary: 'Create (Docker Compose)', + description: 'Create new application based on a docker-compose file.', + path: '/applications/dockercompose', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application object that needs to be created.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['project_uuid', 'server_uuid', 'environment_name', 'docker_compose_raw'], + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID if the server has more than one destinations.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application created successfully.', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_dockercompose_application(Request $request) + { + return $this->create_application($request, 'dockercompose'); + } + + private function create_application(Request $request, $type) + { + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + $serverUuid = $request->server_uuid; + $fqdn = $request->domains; + $instantDeploy = $request->instant_deploy; + $githubAppUuid = $request->github_app_uuid; + + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + if ($type === 'public') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'docker_compose_location' => 'string', + 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_custom_start_command' => 'string|nullable', + 'docker_compose_custom_build_command' => 'string|nullable', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $dockerComposeDomainsJson = collect(); + if ($request->has('docker_compose_domains')) { + $dockerComposeDomains = collect($request->docker_compose_domains); + if ($dockerComposeDomains->count() > 0) { + $dockerComposeDomains->each(function ($domain, $key) use ($dockerComposeDomainsJson) { + $dockerComposeDomainsJson->put(data_get($domain, 'name'), ['domain' => data_get($domain, 'domain')]); + }); + } + $request->offsetUnset('docker_compose_domains'); + } + if ($dockerComposeDomainsJson->count() > 0) { + $application->docker_compose_domains = $dockerComposeDomainsJson; + } + + $application->fqdn = $fqdn; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->save(); + $application->refresh(); + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } else { + if ($application->build_pack === 'dockercompose') { + LoadComposeFile::dispatch($application); + } + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'private-gh-app') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'github_app_uuid' => 'string|required', + 'watch_paths' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_custom_start_command' => 'string|nullable', + 'docker_compose_custom_build_command' => 'string|nullable', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $githubApp = GithubApp::whereTeamId($teamId)->where('uuid', $githubAppUuid)->first(); + if (! $githubApp) { + return response()->json(['message' => 'Github App not found.'], 404); + } + $gitRepository = $request->git_repository; + if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) { + $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', ''); + } + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + + $dockerComposeDomainsJson = collect(); + if ($request->has('docker_compose_domains')) { + $yaml = Yaml::parse($application->docker_compose_raw); + $services = data_get($yaml, 'services'); + $dockerComposeDomains = collect($request->docker_compose_domains); + if ($dockerComposeDomains->count() > 0) { + $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { + $name = data_get($domain, 'name'); + if (data_get($services, $name)) { + $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); + } + }); + } + $request->offsetUnset('docker_compose_domains'); + } + if ($dockerComposeDomainsJson->count() > 0) { + $application->docker_compose_domains = $dockerComposeDomainsJson; + } + $application->fqdn = $fqdn; + $application->git_repository = $gitRepository; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->source_type = $githubApp->getMorphClass(); + $application->source_id = $githubApp->id; + $application->save(); + $application->refresh(); + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } else { + if ($application->build_pack === 'dockercompose') { + LoadComposeFile::dispatch($application); + } + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'private-deploy-key') { + if (! $request->has('name')) { + $request->offsetSet('name', generate_application_name($request->git_repository, $request->git_branch)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'git_repository' => 'string|required', + 'git_branch' => 'string|required', + 'build_pack' => ['required', Rule::enum(BuildPackTypes::class)], + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + 'private_key_uuid' => 'string|required', + 'watch_paths' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_custom_start_command' => 'string|nullable', + 'docker_compose_custom_build_command' => 'string|nullable', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $privateKey = PrivateKey::whereTeamId($teamId)->where('uuid', $request->private_key_uuid)->first(); + if (! $privateKey) { + return response()->json(['message' => 'Private Key not found.'], 404); + } + + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + + $dockerComposeDomainsJson = collect(); + if ($request->has('docker_compose_domains')) { + $yaml = Yaml::parse($application->docker_compose_raw); + $services = data_get($yaml, 'services'); + $dockerComposeDomains = collect($request->docker_compose_domains); + if ($dockerComposeDomains->count() > 0) { + $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { + $name = data_get($domain, 'name'); + if (data_get($services, $name)) { + $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); + } + }); + } + $request->offsetUnset('docker_compose_domains'); + } + if ($dockerComposeDomainsJson->count() > 0) { + $application->docker_compose_domains = $dockerComposeDomainsJson; + } + $application->fqdn = $fqdn; + $application->private_key_id = $privateKey->id; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + $application->save(); + $application->refresh(); + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } else { + if ($application->build_pack === 'dockercompose') { + LoadComposeFile::dispatch($application); + } + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'dockerfile') { + if (! $request->has('name')) { + $request->offsetSet('name', 'dockerfile-'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'dockerfile' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->dockerfile)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + if (mb_detect_encoding($dockerFile, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'dockerfile' => 'The dockerfile should be base64 encoded.', + ], + ], 422); + } + $dockerFile = base64_decode($request->dockerfile); + removeUnnecessaryFieldsFromRequest($request); + + $port = get_port_from_dockerfile($request->dockerfile); + if (! $port) { + $port = 80; + } + + $application = new Application(); + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->ports_exposes = $port; + $application->build_pack = 'dockerfile'; + $application->dockerfile = $dockerFile; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + $application->refresh(); + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'dockerimage') { + if (! $request->has('name')) { + $request->offsetSet('name', 'docker-image-'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'docker_registry_image_name' => 'string|required', + 'docker_registry_image_tag' => 'string', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! $request->docker_registry_image_tag) { + $request->offsetSet('docker_registry_image_tag', 'latest'); + } + $application = new Application(); + removeUnnecessaryFieldsFromRequest($request); + + $application->fill($request->all()); + $application->fqdn = $fqdn; + $application->build_pack = 'dockerimage'; + $application->destination_id = $destination->id; + $application->destination_type = $destination->getMorphClass(); + $application->environment_id = $environment->id; + + $application->git_repository = 'coollabsio/coolify'; + $application->git_branch = 'main'; + $application->save(); + $application->refresh(); + $application->custom_labels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->save(); + $application->isConfigurationChanged(true); + + if ($instantDeploy) { + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + no_questions_asked: true, + is_api: true, + ); + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($application, 'uuid'), + 'domains' => data_get($application, 'domains'), + ])); + } elseif ($type === 'dockercompose') { + $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'instant_deploy', 'docker_compose_raw']; + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (! $request->has('name')) { + $request->offsetSet('name', 'service'.new Cuid2(7)); + } + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'docker_compose_raw' => 'string|required', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + if (! isBase64Encoded($request->docker_compose_raw)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerComposeRaw = base64_decode($request->docker_compose_raw); + if (mb_detect_encoding($dockerComposeRaw, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'docker_compose_raw' => 'The docker_compose_raw should be base64 encoded.', + ], + ], 422); + } + $dockerCompose = base64_decode($request->docker_compose_raw); + $dockerComposeRaw = Yaml::dump(Yaml::parse($dockerCompose), 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); + + // $isValid = validateComposeFile($dockerComposeRaw, $server_id); + // if ($isValid !== 'OK') { + // return $this->dispatch('error', "Invalid docker-compose file.\n$isValid"); + // } + + $service = new Service(); + removeUnnecessaryFieldsFromRequest($request); + $service->fill($request->all()); + + $service->docker_compose_raw = $dockerComposeRaw; + $service->environment_id = $environment->id; + $service->server_id = $server->id; + $service->destination_id = $destination->id; + $service->destination_type = $destination->getMorphClass(); + $service->save(); + + $service->name = "service-$service->uuid"; + $service->parse(isNew: true); + if ($instantDeploy) { + StartService::dispatch($service); + } + + return response()->json(serializeApiResponse([ + 'uuid' => data_get($service, 'uuid'), + 'domains' => data_get($service, 'domains'), + ])); + } + + return response()->json(['message' => 'Invalid type.'], 400); + + } + + #[OA\Get( + summary: 'Get', + description: 'Get application by UUID.', + path: '/applications/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get application by UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + ref: '#/components/schemas/Application' + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function application_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + + return response()->json($this->removeSensitiveData($application)); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete application by UUID.', + path: '/applications/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Application deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Application deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + $cleanup = $request->query->get('cleanup') ?? false; + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + if ($request->collect()->count() == 0) { + return response()->json([ + 'message' => 'Invalid request.', + ], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + DeleteResourceJob::dispatch($application, $cleanup); + + return response()->json([ + 'message' => 'Application deletion request queued.', + ]); + } + + #[OA\Patch( + summary: 'Update', + description: 'Update application by UUID.', + path: '/applications', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + requestBody: new OA\RequestBody( + description: 'Application updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'project_uuid' => ['type' => 'string', 'description' => 'The project UUID.'], + 'server_uuid' => ['type' => 'string', 'description' => 'The server UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'The environment name.'], + 'github_app_uuid' => ['type' => 'string', 'description' => 'The Github App UUID.'], + 'git_repository' => ['type' => 'string', 'description' => 'The git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'The git branch.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], + 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'description' => ['type' => 'string', 'description' => 'The application description.'], + 'domains' => ['type' => 'string', 'description' => 'The application domains.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'The git commit SHA.'], + 'docker_registry_image_name' => ['type' => 'string', 'description' => 'The docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'description' => 'The docker registry image tag.'], + 'is_static' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application is static.'], + 'install_command' => ['type' => 'string', 'description' => 'The install command.'], + 'build_command' => ['type' => 'string', 'description' => 'The build command.'], + 'start_command' => ['type' => 'string', 'description' => 'The start command.'], + 'ports_mappings' => ['type' => 'string', 'description' => 'The ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'The base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'The publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'custom_labels' => ['type' => 'string', 'description' => 'Custom labels.'], + 'custom_docker_run_options' => ['type' => 'string', 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'description' => 'Pre deployment command container.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'description' => 'Manual webhook secret for Github.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitlab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], + 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'The Docker Compose location.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The Docker Compose raw content.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'description' => 'The Docker Compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], + 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], + 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], + ], + )), + ]), + responses: [ + new OA\Response( + response: 200, + description: 'Application updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + if ($request->collect()->count() == 0) { + return response()->json([ + 'message' => 'Invalid request.', + ], 400); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + $server = $application->destination->server; + $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect']; + + $validator = customApiValidator($request->all(), [ + sharedDataApplications(), + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'static_image' => 'string', + 'watch_paths' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_custom_start_command' => 'string|nullable', + 'docker_compose_custom_build_command' => 'string|nullable', + ]); + + // Validate ports_exposes + if ($request->has('ports_exposes')) { + $ports = explode(',', $request->ports_exposes); + foreach ($ports as $port) { + if (! is_numeric($port)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_exposes' => 'The ports_exposes should be a comma separated list of numbers.', + ], + ], 422); + } + } + } + $return = $this->validateDataApplications($request, $server); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $domains = $request->domains; + if ($request->has('domains') && $server->isProxyShouldRun()) { + $errors = []; + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $application->fqdn = $fqdn; + $customLabels = str(implode('|coolify|', generateLabelsApplication($application)))->replace('|coolify|', "\n"); + $application->custom_labels = base64_encode($customLabels); + $request->offsetUnset('domains'); + } + + $dockerComposeDomainsJson = collect(); + if ($request->has('docker_compose_domains')) { + $yaml = Yaml::parse($application->docker_compose_raw); + $services = data_get($yaml, 'services'); + $dockerComposeDomains = collect($request->docker_compose_domains); + if ($dockerComposeDomains->count() > 0) { + $dockerComposeDomains->each(function ($domain, $key) use ($services, $dockerComposeDomainsJson) { + $name = data_get($domain, 'name'); + if (data_get($services, $name)) { + $dockerComposeDomainsJson->put($name, ['domain' => data_get($domain, 'domain')]); + } + }); + } + $request->offsetUnset('docker_compose_domains'); + } + $data = $request->all(); + data_set($data, 'fqdn', $domains); + if ($dockerComposeDomainsJson->count() > 0) { + data_set($data, 'docker_compose_domains', json_encode($dockerComposeDomainsJson)); + } + $application->fill($data); + $application->save(); + + return response()->json([ + 'uuid' => $application->uuid, + ]); + } + + #[OA\Get( + summary: 'List Envs', + description: 'List all envs by application UUID.', + path: '/applications/{uuid}/envs', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'All environment variables by application UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function envs(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + $envs = $application->environment_variables->sortBy('id')->merge($application->environment_variables_preview->sortBy('id')); + + $envs = $envs->map(function ($env) { + $env->makeHidden([ + 'service_id', + 'standalone_clickhouse_id', + 'standalone_dragonfly_id', + 'standalone_keydb_id', + 'standalone_mariadb_id', + 'standalone_mongodb_id', + 'standalone_mysql_id', + 'standalone_postgresql_id', + 'standalone_redis_id', + ]); + $env = $this->removeSensitiveData($env); + + return $env; + }); + + return response()->json($envs); + } + + #[OA\Patch( + summary: 'Update Env', + description: 'Update env by application UUID.', + path: '/applications/{uuid}/envs', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Env updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['key', 'value'], + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variable updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variable updated.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_env_by_uuid(Request $request) + { + $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $is_preview = $request->is_preview ?? false; + $is_build_time = $request->is_build_time ?? false; + $is_literal = $request->is_literal ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $request->key)->first(); + if ($env) { + $env->value = $request->value; + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_preview != $is_preview) { + $env->is_preview = $is_preview; + } + if ($env->is_multiline != $request->is_multiline) { + $env->is_multiline = $request->is_multiline; + } + if ($env->is_shown_once != $request->is_shown_once) { + $env->is_shown_once = $request->is_shown_once; + } + $env->save(); + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } else { + return response()->json([ + 'message' => 'Environment variable not found.', + ], 404); + } + } else { + $env = $application->environment_variables->where('key', $request->key)->first(); + if ($env) { + $env->value = $request->value; + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_preview != $is_preview) { + $env->is_preview = $is_preview; + } + if ($env->is_multiline != $request->is_multiline) { + $env->is_multiline = $request->is_multiline; + } + if ($env->is_shown_once != $request->is_shown_once) { + $env->is_shown_once = $request->is_shown_once; + } + $env->save(); + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } else { + + return response()->json([ + 'message' => 'Environment variable not found.', + ], 404); + + } + } + + return response()->json([ + 'message' => 'Something is not okay. Are you okay?', + ], 500); + + } + + #[OA\Patch( + summary: 'Update Envs (Bulk)', + description: 'Update multiple envs by application UUID.', + path: '/applications/{uuid}/envs/bulk', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Bulk envs updated.', + required: true, + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['data'], + properties: [ + 'data' => [ + 'type' => 'array', + 'items' => new OA\Schema( + type: 'object', + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ], + ], + ), + ), + ], + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variables updated.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variables updated.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_bulk_envs(Request $request) + { + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + + $bulk_data = $request->get('data'); + if (! $bulk_data) { + return response()->json([ + 'message' => 'Bulk data is required.', + ], 400); + } + $bulk_data = collect($bulk_data)->map(function ($item) { + return collect($item)->only(['key', 'value', 'is_preview', 'is_build_time', 'is_literal']); + }); + foreach ($bulk_data as $item) { + $validator = customApiValidator($item, [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $is_preview = $item->get('is_preview') ?? false; + $is_build_time = $item->get('is_build_time') ?? false; + $is_literal = $item->get('is_literal') ?? false; + $is_multi_line = $item->get('is_multiline') ?? false; + $is_shown_once = $item->get('is_shown_once') ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $item->get('key'))->first(); + if ($env) { + $env->value = $item->get('value'); + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_multiline != $item->get('is_multiline')) { + $env->is_multiline = $item->get('is_multiline'); + } + if ($env->is_shown_once != $item->get('is_shown_once')) { + $env->is_shown_once = $item->get('is_shown_once'); + } + $env->save(); + } else { + $env = $application->environment_variables()->create([ + 'key' => $item->get('key'), + 'value' => $item->get('value'), + 'is_preview' => $is_preview, + 'is_build_time' => $is_build_time, + 'is_literal' => $is_literal, + 'is_multiline' => $is_multi_line, + 'is_shown_once' => $is_shown_once, + ]); + } + } else { + $env = $application->environment_variables->where('key', $item->get('key'))->first(); + if ($env) { + $env->value = $item->get('value'); + if ($env->is_build_time != $is_build_time) { + $env->is_build_time = $is_build_time; + } + if ($env->is_literal != $is_literal) { + $env->is_literal = $is_literal; + } + if ($env->is_multiline != $item->get('is_multiline')) { + $env->is_multiline = $item->get('is_multiline'); + } + if ($env->is_shown_once != $item->get('is_shown_once')) { + $env->is_shown_once = $item->get('is_shown_once'); + } + $env->save(); + } else { + $env = $application->environment_variables()->create([ + 'key' => $item->get('key'), + 'value' => $item->get('value'), + 'is_preview' => $is_preview, + 'is_build_time' => $is_build_time, + 'is_literal' => $is_literal, + 'is_multiline' => $is_multi_line, + 'is_shown_once' => $is_shown_once, + ]); + } + } + } + + return response()->json($this->removeSensitiveData($env))->setStatusCode(201); + } + + #[OA\Post( + summary: 'Create Env', + description: 'Create env by application UUID.', + path: '/applications/{uuid}/envs', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + required: true, + description: 'Env created.', + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'key' => ['type' => 'string', 'description' => 'The key of the environment variable.'], + 'value' => ['type' => 'string', 'description' => 'The value of the environment variable.'], + 'is_preview' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in preview deployments.'], + 'is_build_time' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is used in build time.'], + 'is_literal' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is a literal, nothing espaced.'], + 'is_multiline' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable is multiline.'], + 'is_shown_once' => ['type' => 'boolean', 'description' => 'The flag to indicate if the environment variable\'s value is shown on the UI.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Environment variable created.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'example' => 'nc0k04gk8g0cgsk440g0koko'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function create_env(Request $request) + { + $allowedFields = ['key', 'value', 'is_preview', 'is_build_time', 'is_literal']; + $teamId = getTeamIdFromToken(); + + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found', + ], 404); + } + $validator = customApiValidator($request->all(), [ + 'key' => 'string|required', + 'value' => 'string|nullable', + 'is_preview' => 'boolean', + 'is_build_time' => 'boolean', + 'is_literal' => 'boolean', + 'is_multiline' => 'boolean', + 'is_shown_once' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $is_preview = $request->is_preview ?? false; + if ($is_preview) { + $env = $application->environment_variables_preview->where('key', $request->key)->first(); + if ($env) { + return response()->json([ + 'message' => 'Environment variable already exists. Use PATCH request to update it.', + ], 409); + } else { + $env = $application->environment_variables()->create([ + 'key' => $request->key, + 'value' => $request->value, + 'is_preview' => $request->is_preview ?? false, + 'is_build_time' => $request->is_build_time ?? false, + 'is_literal' => $request->is_literal ?? false, + 'is_multiline' => $request->is_multiline ?? false, + 'is_shown_once' => $request->is_shown_once ?? false, + ]); + + return response()->json([ + 'uuid' => $env->uuid, + ])->setStatusCode(201); + } + } else { + $env = $application->environment_variables->where('key', $request->key)->first(); + if ($env) { + return response()->json([ + 'message' => 'Environment variable already exists. Use PATCH request to update it.', + ], 409); + } else { + $env = $application->environment_variables()->create([ + 'key' => $request->key, + 'value' => $request->value, + 'is_preview' => $request->is_preview ?? false, + 'is_build_time' => $request->is_build_time ?? false, + 'is_literal' => $request->is_literal ?? false, + 'is_multiline' => $request->is_multiline ?? false, + 'is_shown_once' => $request->is_shown_once ?? false, + ]); + + return response()->json([ + 'uuid' => $env->uuid, + ])->setStatusCode(201); + + } + } + + return response()->json([ + 'message' => 'Something went wrong.', + ], 500); + + } + + #[OA\Delete( + summary: 'Delete Env', + description: 'Delete env by UUID.', + path: '/applications/{uuid}/envs/{env_uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'env_uuid', + in: 'path', + description: 'UUID of the environment variable.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Environment variable deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Environment variable deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_env_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + + if (! $application) { + return response()->json([ + 'message' => 'Application not found.', + ], 404); + } + $found_env = EnvironmentVariable::where('uuid', $request->env_uuid)->where('application_id', $application->id)->first(); + if (! $found_env) { + return response()->json([ + 'message' => 'Environment variable not found.', + ], 404); + } + $found_env->forceDelete(); + + return response()->json([ + 'message' => 'Environment variable deleted.', + ]); + } + + #[OA\Get( + summary: 'Start', + description: 'Start application. `Post` request is also accepted.', + path: '/applications/{uuid}/start', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + new OA\Parameter( + name: 'force', + in: 'query', + description: 'Force rebuild.', + schema: new OA\Schema( + type: 'boolean', + default: false, + ) + ), + new OA\Parameter( + name: 'instant_deploy', + in: 'query', + description: 'Instant deploy (skip queuing).', + schema: new OA\Schema( + type: 'boolean', + default: false, + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Start application.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Deployment request queued.', 'description' => 'Message.'], + 'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $force = $request->query->get('force') ?? false; + $instant_deploy = $request->query->get('instant_deploy') ?? false; + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: $force, + is_api: true, + no_questions_asked: $instant_deploy + ); + + return response()->json( + [ + 'message' => 'Deployment request queued.', + 'deployment_uuid' => $deployment_uuid->toString(), + ], + 200 + ); + } + + #[OA\Get( + summary: 'Stop', + description: 'Stop application. `Post` request is also accepted.', + path: '/applications/{uuid}/stop', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Stop application.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Application stopping request queued.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_stop(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + StopApplication::dispatch($application); + + return response()->json( + [ + 'message' => 'Application stopping request queued.', + ], + ); + } + + #[OA\Get( + summary: 'Restart', + description: 'Restart application. `Post` request is also accepted.', + path: '/applications/{uuid}/restart', + security: [ + ['bearerAuth' => []], + ], + tags: ['Applications'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the application.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Restart application.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Restart request queued.'], + 'deployment_uuid' => ['type' => 'string', 'example' => 'doogksw', 'description' => 'UUID of the deployment.'], + ] + ) + ), + ]), + + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_restart(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first(); + if (! $application) { + return response()->json(['message' => 'Application not found.'], 404); + } + + $deployment_uuid = new Cuid2(7); + + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + restart_only: true, + is_api: true, + ); + + return response()->json( + [ + 'message' => 'Restart request queued.', + 'deployment_uuid' => $deployment_uuid->toString(), + ], + ); + + } + + private function validateDataApplications(Request $request, Server $server) + { + $teamId = getTeamIdFromToken(); + + // Validate ports_mappings + if ($request->has('ports_mappings')) { + $ports = []; + foreach (explode(',', $request->ports_mappings) as $portMapping) { + $port = explode(':', $portMapping); + if (in_array($port[0], $ports)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'ports_mappings' => 'The first number before : should be unique between mappings.', + ], + ], 422); + } + $ports[] = $port[0]; + } + } + // Validate custom_labels + if ($request->has('custom_labels')) { + if (! isBase64Encoded($request->custom_labels)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + } + $customLabels = base64_decode($request->custom_labels); + if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'custom_labels' => 'The custom_labels should be base64 encoded.', + ], + ], 422); + + } + } + if ($request->has('domains') && $server->isProxyShouldRun()) { + $uuid = $request->uuid; + $fqdn = $request->domains; + $fqdn = str($fqdn)->replaceEnd(',', '')->trim(); + $fqdn = str($fqdn)->replaceStart(',', '')->trim(); + $errors = []; + $fqdn = str($fqdn)->trim()->explode(',')->map(function ($domain) use (&$errors) { + if (filter_var($domain, FILTER_VALIDATE_URL) === false) { + $errors[] = 'Invalid domain: '.$domain; + } + + return str($domain)->trim()->lower(); + }); + if (count($errors) > 0) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (checkIfDomainIsAlreadyUsed($fqdn, $teamId, $uuid)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'domains' => 'One of the domain is already used.', + ], + ], 422); + } + } + } +} diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php new file mode 100644 index 000000000..ef531568c --- /dev/null +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -0,0 +1,1804 @@ +user()->currentAccessToken(); + $database->makeHidden([ + 'id', + 'laravel_through_key', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($database); + } + + $database->makeHidden([ + 'internal_db_url', + 'external_db_url', + 'postgres_password', + 'dragonfly_password', + 'redis_password', + 'mongo_initdb_root_password', + 'keydb_password', + 'clickhouse_admin_password', + ]); + + return serializeApiResponse($database); + } + + #[OA\Get( + summary: 'List', + description: 'List all databases.', + path: '/databases', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all databases', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function databases(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::where('team_id', $teamId)->get(); + $databases = collect(); + foreach ($projects as $project) { + $databases = $databases->merge($project->databases()); + } + $databases = $databases->map(function ($database) { + return $this->removeSensitiveData($database); + }); + + return response()->json($databases); + } + + #[OA\Get( + summary: 'Get', + description: 'Get database by UUID.', + path: '/databases/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get all databases', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function database_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + + return response()->json($this->removeSensitiveData($database)); + } + + #[OA\Patch( + summary: 'Update', + description: 'Update database by UUID.', + path: '/databases/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'], + 'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'], + 'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'], + 'postgres_initdb_args' => ['type' => 'string', 'description' => 'PostgreSQL initdb args'], + 'postgres_host_auth_method' => ['type' => 'string', 'description' => 'PostgreSQL host auth method'], + 'postgres_conf' => ['type' => 'string', 'description' => 'PostgreSQL conf'], + 'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'], + 'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'], + 'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'], + 'redis_password' => ['type' => 'string', 'description' => 'Redis password'], + 'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'], + 'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'], + 'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'], + 'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'], + 'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'], + 'mariadb_user' => ['type' => 'string', 'description' => 'MariaDB user'], + 'mariadb_password' => ['type' => 'string', 'description' => 'MariaDB password'], + 'mariadb_database' => ['type' => 'string', 'description' => 'MariaDB database'], + 'mongo_conf' => ['type' => 'string', 'description' => 'Mongo conf'], + 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'Mongo initdb root username'], + 'mongo_initdb_root_password' => ['type' => 'string', 'description' => 'Mongo initdb root password'], + 'mongo_initdb_init_database' => ['type' => 'string', 'description' => 'Mongo initdb init database'], + 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], + 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], + 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], + 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function update_by_uuid(Request $request) + { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'image' => 'string', + 'is_public' => 'boolean', + 'public_port' => 'numeric|nullable', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + ]); + + if ($validator->fails()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + $uuid = $request->uuid; + removeUnnecessaryFieldsFromRequest($request); + $database = queryDatabaseByUuidWithinTeam($uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + if ($request->is_public && $request->public_port) { + if (isPublicPortAlreadyUsed($database->destination->server, $request->public_port, $database->id)) { + return response()->json(['message' => 'Public port already used by another database.'], 400); + } + } + switch ($database->type()) { + case 'standalone-postgresql': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; + $validator = customApiValidator($request->all(), [ + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'postgres_initdb_args' => 'string', + 'postgres_host_auth_method' => 'string', + 'postgres_conf' => 'string', + ]); + if ($request->has('postgres_conf')) { + if (! isBase64Encoded($request->postgres_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $postgresConf = base64_decode($request->postgres_conf); + if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('postgres_conf', $postgresConf); + } + break; + case 'standalone-clickhouse': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; + $validator = customApiValidator($request->all(), [ + 'clickhouse_admin_user' => 'string', + 'clickhouse_admin_password' => 'string', + ]); + break; + case 'standalone-dragonfly': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; + $validator = customApiValidator($request->all(), [ + 'dragonfly_password' => 'string', + ]); + break; + case 'standalone-redis': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; + $validator = customApiValidator($request->all(), [ + 'redis_password' => 'string', + 'redis_conf' => 'string', + ]); + if ($request->has('redis_conf')) { + if (! isBase64Encoded($request->redis_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $redisConf = base64_decode($request->redis_conf); + if (mb_detect_encoding($redisConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('redis_conf', $redisConf); + } + break; + case 'standalone-keydb': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; + $validator = customApiValidator($request->all(), [ + 'keydb_password' => 'string', + 'keydb_conf' => 'string', + ]); + if ($request->has('keydb_conf')) { + if (! isBase64Encoded($request->keydb_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $keydbConf = base64_decode($request->keydb_conf); + if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('keydb_conf', $keydbConf); + } + break; + case 'standalone-mariadb': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; + $validator = customApiValidator($request->all(), [ + 'mariadb_conf' => 'string', + 'mariadb_root_password' => 'string', + 'mariadb_user' => 'string', + 'mariadb_password' => 'string', + 'mariadb_database' => 'string', + ]); + if ($request->has('mariadb_conf')) { + if (! isBase64Encoded($request->mariadb_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $mariadbConf = base64_decode($request->mariadb_conf); + if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mariadb_conf', $mariadbConf); + } + break; + case 'standalone-mongodb': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; + $validator = customApiValidator($request->all(), [ + 'mongo_conf' => 'string', + 'mongo_initdb_root_username' => 'string', + 'mongo_initdb_root_password' => 'string', + 'mongo_initdb_init_database' => 'string', + ]); + if ($request->has('mongo_conf')) { + if (! isBase64Encoded($request->mongo_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $mongoConf = base64_decode($request->mongo_conf); + if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mongo_conf', $mongoConf); + } + + break; + case 'standalone-mysql': + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $validator = customApiValidator($request->all(), [ + 'mysql_root_password' => 'string', + 'mysql_user' => 'string', + 'mysql_database' => 'string', + 'mysql_conf' => 'string', + ]); + if ($request->has('mysql_conf')) { + if (! isBase64Encoded($request->mysql_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $mysqlConf = base64_decode($request->mysql_conf); + if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mysql_conf', $mysqlConf); + } + break; + + } + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $whatToDoWithDatabaseProxy = null; + if ($request->is_public === false && $database->is_public === true) { + $whatToDoWithDatabaseProxy = 'stop'; + } + if ($request->is_public === true && $request->public_port && $database->is_public === false) { + $whatToDoWithDatabaseProxy = 'start'; + } + + $database->update($request->all()); + + if ($whatToDoWithDatabaseProxy === 'start') { + StartDatabaseProxy::dispatch($database); + } elseif ($whatToDoWithDatabaseProxy === 'stop') { + StopDatabaseProxy::dispatch($database); + } + + return response()->json([ + 'message' => 'Database updated.', + ]); + + } + + #[OA\Post( + summary: 'Create (PostgreSQL)', + description: 'Create a new PostgreSQL database.', + path: '/databases/postgresql', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'postgres_user' => ['type' => 'string', 'description' => 'PostgreSQL user'], + 'postgres_password' => ['type' => 'string', 'description' => 'PostgreSQL password'], + 'postgres_db' => ['type' => 'string', 'description' => 'PostgreSQL database'], + 'postgres_initdb_args' => ['type' => 'string', 'description' => 'PostgreSQL initdb args'], + 'postgres_host_auth_method' => ['type' => 'string', 'description' => 'PostgreSQL host auth method'], + 'postgres_conf' => ['type' => 'string', 'description' => 'PostgreSQL conf'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_postgresql(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::POSTGRESQL); + } + + #[OA\Post( + summary: 'Create (Clickhouse)', + description: 'Create a new Clickhouse database.', + path: '/databases/clickhouse', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'clickhouse_admin_user' => ['type' => 'string', 'description' => 'Clickhouse admin user'], + 'clickhouse_admin_password' => ['type' => 'string', 'description' => 'Clickhouse admin password'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_clickhouse(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::CLICKHOUSE); + } + + #[OA\Post( + summary: 'Create (DragonFly)', + description: 'Create a new DragonFly database.', + path: '/databases/dragonfly', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'dragonfly_password' => ['type' => 'string', 'description' => 'DragonFly password'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_dragonfly(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::DRAGONFLY); + } + + #[OA\Post( + summary: 'Create (Redis)', + description: 'Create a new Redis database.', + path: '/databases/redis', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'redis_password' => ['type' => 'string', 'description' => 'Redis password'], + 'redis_conf' => ['type' => 'string', 'description' => 'Redis conf'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_redis(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::REDIS); + } + + #[OA\Post( + summary: 'Create (KeyDB)', + description: 'Create a new KeyDB database.', + path: '/databases/keydb', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'keydb_password' => ['type' => 'string', 'description' => 'KeyDB password'], + 'keydb_conf' => ['type' => 'string', 'description' => 'KeyDB conf'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_keydb(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::KEYDB); + } + + #[OA\Post( + summary: 'Create (MariaDB)', + description: 'Create a new MariaDB database.', + path: '/databases/mariadb', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'mariadb_conf' => ['type' => 'string', 'description' => 'MariaDB conf'], + 'mariadb_root_password' => ['type' => 'string', 'description' => 'MariaDB root password'], + 'mariadb_user' => ['type' => 'string', 'description' => 'MariaDB user'], + 'mariadb_password' => ['type' => 'string', 'description' => 'MariaDB password'], + 'mariadb_database' => ['type' => 'string', 'description' => 'MariaDB database'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_mariadb(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::MARIADB); + } + + #[OA\Post( + summary: 'Create (MySQL)', + description: 'Create a new MySQL database.', + path: '/databases/mysql', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'mysql_root_password' => ['type' => 'string', 'description' => 'MySQL root password'], + 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], + 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], + 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_mysql(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::MYSQL); + } + + #[OA\Post( + summary: 'Create (MongoDB)', + description: 'Create a new MongoDB database.', + path: '/databases/mongodb', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'mongo_conf' => ['type' => 'string', 'description' => 'MongoDB conf'], + 'mongo_initdb_root_username' => ['type' => 'string', 'description' => 'MongoDB initdb root username'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'is_public' => ['type' => 'boolean', 'description' => 'Is the database public?'], + 'public_port' => ['type' => 'integer', 'description' => 'Public port of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_database_mongodb(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::MONGODB); + } + + public function create_database(Request $request, NewDatabaseTypes $type) + { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database', 'mysql_root_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if (! empty($extraFields)) { + $errors = collect([]); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $serverUuid = $request->server_uuid; + $instantDeploy = $request->instant_deploy ?? false; + if ($request->is_public && ! $request->public_port) { + $request->offsetSet('is_public', false); + } + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + if ($request->has('public_port') && $request->is_public) { + if (isPublicPortAlreadyUsed($server, $request->public_port)) { + return response()->json(['message' => 'Public port already used by another database.'], 400); + } + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'image' => 'string', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'is_public' => 'boolean', + 'public_port' => 'numeric|nullable', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'instant_deploy' => 'boolean', + ]); + if ($validator->failed()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $validator->errors(), + ], 422); + } + if ($request->public_port) { + if ($request->public_port < 1024 || $request->public_port > 65535) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'public_port' => 'The public port should be between 1024 and 65535.', + ], + ], 422); + } + } + if ($type === NewDatabaseTypes::POSTGRESQL) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf']; + $validator = customApiValidator($request->all(), [ + 'postgres_user' => 'string', + 'postgres_password' => 'string', + 'postgres_db' => 'string', + 'postgres_initdb_args' => 'string', + 'postgres_host_auth_method' => 'string', + 'postgres_conf' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('postgres_conf')) { + if (! isBase64Encoded($request->postgres_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $postgresConf = base64_decode($request->postgres_conf); + if (mb_detect_encoding($postgresConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'postgres_conf' => 'The postgres_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('postgres_conf', $postgresConf); + } + $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + + } elseif ($type === NewDatabaseTypes::MARIADB) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; + $validator = customApiValidator($request->all(), [ + 'clickhouse_admin_user' => 'string', + 'clickhouse_admin_password' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mariadb_conf')) { + if (! isBase64Encoded($request->mariadb_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $mariadbConf = base64_decode($request->mariadb_conf); + if (mb_detect_encoding($mariadbConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mariadb_conf' => 'The mariadb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mariadb_conf', $mariadbConf); + } + $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::MYSQL) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mysql_user', 'mysql_database', 'mysql_conf']; + $validator = customApiValidator($request->all(), [ + 'mysql_root_password' => 'string', + 'mysql_user' => 'string', + 'mysql_database' => 'string', + 'mysql_conf' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mysql_conf')) { + if (! isBase64Encoded($request->mysql_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $mysqlConf = base64_decode($request->mysql_conf); + if (mb_detect_encoding($mysqlConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mysql_conf' => 'The mysql_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mysql_conf', $mysqlConf); + } + $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::REDIS) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'redis_password', 'redis_conf']; + $validator = customApiValidator($request->all(), [ + 'redis_password' => 'string', + 'redis_conf' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('redis_conf')) { + if (! isBase64Encoded($request->redis_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $redisConf = base64_decode($request->redis_conf); + if (mb_detect_encoding($redisConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'redis_conf' => 'The redis_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('redis_conf', $redisConf); + } + $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::DRAGONFLY) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'dragonfly_password']; + $validator = customApiValidator($request->all(), [ + 'dragonfly_password' => 'string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + + return response()->json(serializeApiResponse([ + 'uuid' => $database->uuid, + ]))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::KEYDB) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'keydb_password', 'keydb_conf']; + $validator = customApiValidator($request->all(), [ + 'keydb_password' => 'string', + 'keydb_conf' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('keydb_conf')) { + if (! isBase64Encoded($request->keydb_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $keydbConf = base64_decode($request->keydb_conf); + if (mb_detect_encoding($keydbConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'keydb_conf' => 'The keydb_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('keydb_conf', $keydbConf); + } + $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::CLICKHOUSE) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'clickhouse_admin_user', 'clickhouse_admin_password']; + $validator = customApiValidator($request->all(), [ + 'clickhouse_admin_user' => 'string', + 'clickhouse_admin_password' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::MONGODB) { + $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_init_database']; + $validator = customApiValidator($request->all(), [ + 'mongo_conf' => 'string', + 'mongo_initdb_root_username' => 'string', + 'mongo_initdb_root_password' => 'string', + 'mongo_initdb_init_database' => 'string', + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + if ($request->has('mongo_conf')) { + if (! isBase64Encoded($request->mongo_conf)) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $mongoConf = base64_decode($request->mongo_conf); + if (mb_detect_encoding($mongoConf, 'ASCII', true) === false) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => [ + 'mongo_conf' => 'The mongo_conf should be base64 encoded.', + ], + ], 422); + } + $request->offsetSet('mongo_conf', $mongoConf); + } + $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + + $database->refresh(); + $payload = [ + 'uuid' => $database->uuid, + 'internal_db_url' => $database->internal_db_url, + ]; + if ($database->is_public && $database->public_port) { + $payload['external_db_url'] = $database->external_db_url; + } + + return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } + + return response()->json(['message' => 'Invalid database type requested.'], 400); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete database by UUID.', + path: '/databases/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Database deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Database deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + StopDatabase::dispatch($database); + $database->forceDelete(); + + return response()->json([ + 'message' => 'Database deletion request queued.', + ]); + } + + #[OA\Get( + summary: 'Start', + description: 'Start database. `Post` request is also accepted.', + path: '/databases/{uuid}/start', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Start database.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Database starting request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + if (str($database->status)->contains('running')) { + return response()->json(['message' => 'Database is already running.'], 400); + } + StartDatabase::dispatch($database); + + return response()->json( + [ + 'message' => 'Database starting request queued.', + ], + 200 + ); + } + + #[OA\Get( + summary: 'Stop', + description: 'Stop database. `Post` request is also accepted.', + path: '/databases/{uuid}/stop', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Stop database.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Database stopping request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_stop(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) { + return response()->json(['message' => 'Database is already stopped.'], 400); + } + StopDatabase::dispatch($database); + + return response()->json( + [ + 'message' => 'Database stopping request queued.', + ], + 200 + ); + } + + #[OA\Get( + summary: 'Restart', + description: 'Restart database. `Post` request is also accepted.', + path: '/databases/{uuid}/restart', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the database.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Restart database.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Database restaring request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_restart(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $database = queryDatabaseByUuidWithinTeam($request->uuid, $teamId); + if (! $database) { + return response()->json(['message' => 'Database not found.'], 404); + } + RestartDatabase::dispatch($database); + + return response()->json( + [ + 'message' => 'Database restarting request queued.', + ], + 200 + ); + + } +} diff --git a/app/Http/Controllers/Api/Deploy.php b/app/Http/Controllers/Api/Deploy.php deleted file mode 100644 index d2abe2e31..000000000 --- a/app/Http/Controllers/Api/Deploy.php +++ /dev/null @@ -1,216 +0,0 @@ -get(); - $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get([ - 'id', - 'application_id', - 'application_name', - 'deployment_url', - 'pull_request_id', - 'server_name', - 'server_id', - 'status', - ])->sortBy('id')->toArray(); - - return response()->json($deployments_per_server, 200); - } - - public function deploy(Request $request) - { - $teamId = get_team_id_from_token(); - $uuids = $request->query->get('uuid'); - $tags = $request->query->get('tag'); - $force = $request->query->get('force') ?? false; - - if ($uuids && $tags) { - return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); - } - if (is_null($teamId)) { - return invalid_token(); - } - if ($tags) { - return $this->by_tags($tags, $teamId, $force); - } elseif ($uuids) { - return $this->by_uuids($uuids, $teamId, $force); - } - - return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); - } - - private function by_uuids(string $uuid, int $teamId, bool $force = false) - { - $uuids = explode(',', $uuid); - $uuids = collect(array_filter($uuids)); - - if (count($uuids) === 0) { - return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); - } - $deployments = collect(); - $payload = collect(); - foreach ($uuids as $uuid) { - $resource = getResourceByUuid($uuid, $teamId); - if ($resource) { - ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); - if ($deployment_uuid) { - $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); - } else { - $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]); - } - } - } - if ($deployments->count() > 0) { - $payload->put('deployments', $deployments->toArray()); - - return response()->json($payload->toArray(), 200); - } - - return response()->json(['error' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); - } - - public function by_tags(string $tags, int $team_id, bool $force = false) - { - $tags = explode(',', $tags); - $tags = collect(array_filter($tags)); - - if (count($tags) === 0) { - return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400); - } - $message = collect([]); - $deployments = collect(); - $payload = collect(); - foreach ($tags as $tag) { - $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first(); - if (! $found_tag) { - // $message->push("Tag {$tag} not found."); - continue; - } - $applications = $found_tag->applications()->get(); - $services = $found_tag->services()->get(); - if ($applications->count() === 0 && $services->count() === 0) { - $message->push("No resources found for tag {$tag}."); - - continue; - } - foreach ($applications as $resource) { - ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); - if ($deployment_uuid) { - $deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]); - } - $message = $message->merge($return_message); - } - foreach ($services as $resource) { - ['message' => $return_message] = $this->deploy_resource($resource, $force); - $message = $message->merge($return_message); - } - } - ray($message); - if ($message->count() > 0) { - $payload->put('message', $message->toArray()); - if ($deployments->count() > 0) { - $payload->put('details', $deployments->toArray()); - } - - return response()->json($payload->toArray(), 200); - } - - return response()->json(['error' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404); - } - - public function deploy_resource($resource, bool $force = false): array - { - $message = null; - $deployment_uuid = null; - if (gettype($resource) !== 'object') { - return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; - } - $type = $resource?->getMorphClass(); - if ($type === 'App\Models\Application') { - $deployment_uuid = new Cuid2(7); - queue_application_deployment( - application: $resource, - deployment_uuid: $deployment_uuid, - force_rebuild: $force, - ); - $message = "Application {$resource->name} deployment queued."; - } elseif ($type === 'App\Models\StandalonePostgresql') { - StartPostgresql::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneRedis') { - StartRedis::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneKeydb') { - StartKeydb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneDragonfly') { - StartDragonfly::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneClickhouse') { - StartClickhouse::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMongodb') { - StartMongodb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMysql') { - StartMysql::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\StandaloneMariadb') { - StartMariadb::run($resource); - $resource->update([ - 'started_at' => now(), - ]); - $message = "Database {$resource->name} started."; - } elseif ($type === 'App\Models\Service') { - StartService::run($resource); - $message = "Service {$resource->name} started. It could take a while, be patient."; - } - - return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; - } -} diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php new file mode 100644 index 000000000..2ee56d0cd --- /dev/null +++ b/app/Http/Controllers/Api/DeployController.php @@ -0,0 +1,317 @@ +user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($deployment); + } + + $deployment->makeHidden([ + 'logs', + ]); + + return serializeApiResponse($deployment); + } + + #[OA\Get( + summary: 'List', + description: 'List currently running deployments', + path: '/deployments', + security: [ + ['bearerAuth' => []], + ], + tags: ['Deployments'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all currently running deployments.', + content: [ + + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/ApplicationDeploymentQueue'), + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function deployments(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $servers = Server::whereTeamId($teamId)->get(); + $deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get()->sortBy('id'); + $deployments_per_server = $deployments_per_server->map(function ($deployment) { + return $this->removeSensitiveData($deployment); + }); + + return response()->json($deployments_per_server); + } + + #[OA\Get( + summary: 'Get', + description: 'Get deployment by UUID.', + path: '/deployments/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Deployments'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get deployment by UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + ref: '#/components/schemas/ApplicationDeploymentQueue', + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function deployment_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first(); + if (! $deployment) { + return response()->json(['message' => 'Deployment not found.'], 404); + } + + return response()->json($this->removeSensitiveData($deployment)); + } + + #[OA\Get( + summary: 'Deploy', + description: 'Deploy by tag or uuid. `Post` request also accepted.', + path: '/deploy', + security: [ + ['bearerAuth' => []], + ], + tags: ['Deployments'], + parameters: [ + new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')), + new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')), + ], + + responses: [ + new OA\Response( + response: 200, + description: 'Get deployment(s) Uuid\'s', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'deployments' => new OA\Property( + property: 'deployments', + type: 'array', + items: new OA\Items( + type: 'object', + properties: [ + 'message' => ['type' => 'string'], + 'resource_uuid' => ['type' => 'string'], + 'deployment_uuid' => ['type' => 'string'], + ] + ), + ), + ], + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + + ] + )] + public function deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + $uuids = $request->query->get('uuid'); + $tags = $request->query->get('tag'); + $force = $request->query->get('force') ?? false; + + if ($uuids && $tags) { + return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400); + } + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($tags) { + return $this->by_tags($tags, $teamId, $force); + } elseif ($uuids) { + return $this->by_uuids($uuids, $teamId, $force); + } + + return response()->json(['message' => 'You must provide uuid or tag.'], 400); + } + + private function by_uuids(string $uuid, int $teamId, bool $force = false) + { + $uuids = explode(',', $uuid); + $uuids = collect(array_filter($uuids)); + + if (count($uuids) === 0) { + return response()->json(['message' => 'No UUIDs provided.'], 400); + } + $deployments = collect(); + $payload = collect(); + foreach ($uuids as $uuid) { + $resource = getResourceByUuid($uuid, $teamId); + if ($resource) { + ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); + if ($deployment_uuid) { + $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]); + } else { + $deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]); + } + } + } + if ($deployments->count() > 0) { + $payload->put('deployments', $deployments->toArray()); + + return response()->json(serializeApiResponse($payload->toArray())); + } + + return response()->json(['message' => 'No resources found.'], 404); + } + + public function by_tags(string $tags, int $team_id, bool $force = false) + { + $tags = explode(',', $tags); + $tags = collect(array_filter($tags)); + + if (count($tags) === 0) { + return response()->json(['message' => 'No TAGs provided.'], 400); + } + $message = collect([]); + $deployments = collect(); + $payload = collect(); + foreach ($tags as $tag) { + $found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first(); + if (! $found_tag) { + // $message->push("Tag {$tag} not found."); + continue; + } + $applications = $found_tag->applications()->get(); + $services = $found_tag->services()->get(); + if ($applications->count() === 0 && $services->count() === 0) { + $message->push("No resources found for tag {$tag}."); + + continue; + } + foreach ($applications as $resource) { + ['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force); + if ($deployment_uuid) { + $deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]); + } + $message = $message->merge($return_message); + } + foreach ($services as $resource) { + ['message' => $return_message] = $this->deploy_resource($resource, $force); + $message = $message->merge($return_message); + } + } + if ($message->count() > 0) { + $payload->put('message', $message->toArray()); + if ($deployments->count() > 0) { + $payload->put('details', $deployments->toArray()); + } + + return response()->json(serializeApiResponse($payload->toArray())); + } + + return response()->json(['message' => 'No resources found with this tag.'], 404); + } + + public function deploy_resource($resource, bool $force = false): array + { + $message = null; + $deployment_uuid = null; + if (gettype($resource) !== 'object') { + return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; + } + switch ($resource?->getMorphClass()) { + case 'App\Models\Application': + $deployment_uuid = new Cuid2(7); + queue_application_deployment( + application: $resource, + deployment_uuid: $deployment_uuid, + force_rebuild: $force, + ); + $message = "Application {$resource->name} deployment queued."; + break; + case 'App\Models\Service': + StartService::run($resource); + $message = "Service {$resource->name} started. It could take a while, be patient."; + break; + default: + // Database resource + StartDatabase::dispatch($resource); + $resource->update([ + 'started_at' => now(), + ]); + $message = "Database {$resource->name} started."; + break; + } + + return ['message' => $message, 'deployment_uuid' => $deployment_uuid]; + } +} diff --git a/app/Http/Controllers/Api/Domains.php b/app/Http/Controllers/Api/Domains.php deleted file mode 100644 index c27ddf620..000000000 --- a/app/Http/Controllers/Api/Domains.php +++ /dev/null @@ -1,104 +0,0 @@ -get(); - $domains = collect(); - $applications = $projects->pluck('applications')->flatten(); - $settings = InstanceSettings::get(); - if ($applications->count() > 0) { - foreach ($applications as $application) { - $ip = $application->destination->server->ip; - $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); - }); - if ($ip === 'host.docker.internal') { - if ($settings->public_ipv4) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv4, - ]); - } - if ($settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv6, - ]); - } - if (! $settings->public_ipv4 && ! $settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } else { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } - } - $services = $projects->pluck('services')->flatten(); - if ($services->count() > 0) { - foreach ($services as $service) { - $service_applications = $service->applications; - if ($service_applications->count() > 0) { - foreach ($service_applications as $application) { - $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { - return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', ''); - }); - if ($ip === 'host.docker.internal') { - if ($settings->public_ipv4) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv4, - ]); - } - if ($settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $settings->public_ipv6, - ]); - } - if (! $settings->public_ipv4 && ! $settings->public_ipv6) { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } else { - $domains->push([ - 'domain' => $fqdn, - 'ip' => $ip, - ]); - } - } - } - } - } - $domains = $domains->groupBy('ip')->map(function ($domain) { - return $domain->pluck('domain')->flatten(); - })->map(function ($domain, $ip) { - return [ - 'ip' => $ip, - 'domains' => $domain, - ]; - })->values(); - - return response()->json($domains); - } -} diff --git a/app/Http/Controllers/Api/EnvironmentVariablesController.php b/app/Http/Controllers/Api/EnvironmentVariablesController.php new file mode 100644 index 000000000..d127d0525 --- /dev/null +++ b/app/Http/Controllers/Api/EnvironmentVariablesController.php @@ -0,0 +1,35 @@ +env_uuid)->first(); + if (! $env) { + return response()->json([ + 'message' => 'Environment variable not found.', + ], 404); + } + $found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first(); + if (! $found_app) { + return response()->json([ + 'message' => 'Environment variable not found.', + ], 404); + } + $env->delete(); + + return response()->json([ + 'message' => 'Environment variable deleted.', + ]); + } +} diff --git a/app/Http/Controllers/Api/OpenApi.php b/app/Http/Controllers/Api/OpenApi.php new file mode 100644 index 000000000..59731ef40 --- /dev/null +++ b/app/Http/Controllers/Api/OpenApi.php @@ -0,0 +1,51 @@ + []], + ], + responses: [ + new OA\Response( + response: 200, + description: 'Returns the version of the application', + content: new OA\JsonContent( + type: 'string', + example: 'v4.0.0', + )), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function version(Request $request) + { + return response(config('version')); + } + + #[OA\Get( + summary: 'Enable API', + description: 'Enable API (only with root permissions).', + path: '/enable', + security: [ + ['bearerAuth' => []], + ], + responses: [ + new OA\Response( + response: 200, + description: 'Enable API.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'API enabled.'), + ] + )), + new OA\Response( + response: 403, + description: 'You are not allowed to enable the API.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to enable the API.'), + ] + )), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function enable_api(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($teamId !== '0') { + return response()->json(['message' => 'You are not allowed to enable the API.'], 403); + } + $settings = InstanceSettings::get(); + $settings->update(['is_api_enabled' => true]); + + return response()->json(['message' => 'API enabled.'], 200); + } + + #[OA\Get( + summary: 'Disable API', + description: 'Disable API (only with root permissions).', + path: '/disable', + security: [ + ['bearerAuth' => []], + ], + responses: [ + new OA\Response( + response: 200, + description: 'Disable API.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'API disabled.'), + ] + )), + new OA\Response( + response: 403, + description: 'You are not allowed to disable the API.', + content: new OA\JsonContent( + type: 'object', + properties: [ + new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to disable the API.'), + ] + )), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function disable_api(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if ($teamId !== '0') { + return response()->json(['message' => 'You are not allowed to disable the API.'], 403); + } + $settings = InstanceSettings::get(); + $settings->update(['is_api_enabled' => false]); + + return response()->json(['message' => 'API disabled.'], 200); + } + + public function feedback(Request $request) + { + $content = $request->input('content'); + $webhook_url = config('coolify.feedback_discord_webhook'); + if ($webhook_url) { + Http::post($webhook_url, [ + 'content' => $content, + ]); + } + + return response()->json(['message' => 'Feedback sent.'], 200); + } + + #[OA\Get( + summary: 'Healthcheck', + description: 'Healthcheck endpoint.', + path: '/healthcheck', + responses: [ + new OA\Response( + response: 200, + description: 'Healthcheck endpoint.', + content: new OA\JsonContent( + type: 'string', + example: 'OK', + )), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function healthcheck(Request $request) + { + return 'OK'; + } +} diff --git a/app/Http/Controllers/Api/Project.php b/app/Http/Controllers/Api/Project.php deleted file mode 100644 index baaf1eacb..000000000 --- a/app/Http/Controllers/Api/Project.php +++ /dev/null @@ -1,44 +0,0 @@ -select('id', 'name', 'uuid')->get(); - - return response()->json($projects); - } - - public function project_by_uuid(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); - - return response()->json($project); - } - - public function environment_details(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); - $environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); - - return response()->json($environment); - } -} diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php new file mode 100644 index 000000000..b7c3d115d --- /dev/null +++ b/app/Http/Controllers/Api/ProjectController.php @@ -0,0 +1,147 @@ + []], + ], + tags: ['Projects'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all projects.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Project') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function projects(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get(); + + return response()->json(serializeApiResponse($projects), + ); + } + + #[OA\Get( + summary: 'Get', + description: 'Get project by Uuid.', + path: '/projects/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Projects'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Project details', + content: new OA\JsonContent(ref: '#/components/schemas/Project')), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + description: 'Project not found.', + ), + ] + )] + public function project_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + + return response()->json( + serializeApiResponse($project), + ); + } + + #[OA\Get( + summary: 'Environment', + description: 'Get environment by name.', + path: '/projects/{uuid}/{environment_name}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Projects'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'integer')), + new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Project details', + content: new OA\JsonContent(ref: '#/components/schemas/Environment')), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function environment_details(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); + $environment = $project->environments()->whereName(request()->environment_name)->first(); + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']); + + return response()->json(serializeApiResponse($environment)); + } +} diff --git a/app/Http/Controllers/Api/Resources.php b/app/Http/Controllers/Api/ResourcesController.php similarity index 51% rename from app/Http/Controllers/Api/Resources.php rename to app/Http/Controllers/Api/ResourcesController.php index 0d538b62e..ae076bb71 100644 --- a/app/Http/Controllers/Api/Resources.php +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -5,14 +5,42 @@ use App\Http\Controllers\Controller; use App\Models\Project; use Illuminate\Http\Request; +use OpenApi\Attributes as OA; -class Resources extends Controller +class ResourcesController extends Controller { + #[OA\Get( + summary: 'List', + description: 'Get all resources.', + path: '/resources', + security: [ + ['bearerAuth' => []], + ], + tags: ['Resources'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all resources', + content: new OA\JsonContent( + type: 'string', + example: 'Content is very complex. Will be implemented later.', + ), + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] public function resources(Request $request) { - $teamId = get_team_id_from_token(); + $teamId = getTeamIdFromToken(); if (is_null($teamId)) { - return invalid_token(); + return invalidTokenResponse(); } $projects = Project::where('team_id', $teamId)->get(); $resources = collect(); @@ -34,6 +62,6 @@ public function resources(Request $request) return $payload; }); - return response()->json($resources); + return response()->json(serializeApiResponse($resources)); } } diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php new file mode 100644 index 000000000..11e8e27ca --- /dev/null +++ b/app/Http/Controllers/Api/SecurityController.php @@ -0,0 +1,372 @@ +user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($team); + } + $team->makeHidden([ + 'private_key', + ]); + + return serializeApiResponse($team); + } + + #[OA\Get( + summary: 'List', + description: 'List all private keys.', + path: '/security/keys', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all private keys.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/PrivateKey') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function keys(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $keys = PrivateKey::where('team_id', $teamId)->get(); + + return response()->json($this->removeSensitiveData($keys)); + } + + #[OA\Get( + summary: 'Get', + description: 'Get key by UUID.', + path: '/security/keys/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get all private keys.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/PrivateKey') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + description: 'Private Key not found.', + ), + ] + )] + public function key_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + + if (is_null($key)) { + return response()->json([ + 'message' => 'Private Key not found.', + ], 404); + } + + return response()->json($this->removeSensitiveData($key)); + } + + #[OA\Post( + summary: 'Create', + description: 'Create a new private key.', + path: '/security/keys', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + requestBody: new OA\RequestBody( + required: true, + content: [ + 'application/json' => new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['private_key'], + properties: [ + 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'private_key' => ['type' => 'string'], + ], + additionalProperties: false, + ) + ), + ] + ), + responses: [ + new OA\Response( + response: 201, + description: 'The created private key\'s UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + if ($validator->fails()) { + $errors = $validator->errors(); + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + if (! $request->name) { + $request->offsetSet('name', generate_random_name()); + } + if (! $request->description) { + $request->offsetSet('description', 'Created by Coolify via API'); + } + $key = PrivateKey::create([ + 'team_id' => $teamId, + 'name' => $request->name, + 'description' => $request->description, + 'private_key' => $request->private_key, + ]); + + return response()->json(serializeApiResponse([ + 'uuid' => $key->uuid, + ]))->setStatusCode(201); + } + + #[OA\Patch( + summary: 'Update', + description: 'Update a private key.', + path: '/security/keys', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + requestBody: new OA\RequestBody( + required: true, + content: [ + 'application/json' => new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['private_key'], + properties: [ + 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'private_key' => ['type' => 'string'], + ], + additionalProperties: false, + ) + ), + ] + ), + responses: [ + new OA\Response( + response: 201, + description: 'The updated private key\'s UUID.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function update_key(Request $request) + { + $allowedFields = ['name', 'description', 'private_key']; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + + $validator = customApiValidator($request->all(), [ + 'name' => 'string|max:255', + 'description' => 'string|max:255', + 'private_key' => 'required|string', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $foundKey = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($foundKey)) { + return response()->json([ + 'message' => 'Private Key not found.', + ], 404); + } + $foundKey->update($request->all()); + + return response()->json(serializeApiResponse([ + 'uuid' => $foundKey->uuid, + ]))->setStatusCode(201); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete a private key.', + path: '/security/keys/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Private Keys'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Private Key deleted.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Private Key deleted.'], + ] + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + description: 'Private Key not found.', + ), + ] + )] + public function delete_key(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 422); + } + + $key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first(); + if (is_null($key)) { + return response()->json(['message' => 'Private Key not found.'], 404); + } + $key->forceDelete(); + + return response()->json([ + 'message' => 'Private Key deleted.', + ]); + } +} diff --git a/app/Http/Controllers/Api/Server.php b/app/Http/Controllers/Api/Server.php deleted file mode 100644 index 9f88a3b28..000000000 --- a/app/Http/Controllers/Api/Server.php +++ /dev/null @@ -1,62 +0,0 @@ -select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { - $server['is_reachable'] = $server->settings->is_reachable; - $server['is_usable'] = $server->settings->is_usable; - - return $server; - }); - - return response()->json($servers); - } - - public function server_by_uuid(Request $request) - { - $with_resources = $request->query('resources'); - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); - if (is_null($server)) { - return response()->json(['error' => 'Server not found.'], 404); - } - if ($with_resources) { - $server['resources'] = $server->definedResources()->map(function ($resource) { - $payload = [ - 'id' => $resource->id, - 'uuid' => $resource->uuid, - 'name' => $resource->name, - 'type' => $resource->type(), - 'created_at' => $resource->created_at, - 'updated_at' => $resource->updated_at, - ]; - if ($resource->type() === 'service') { - $payload['status'] = $resource->status(); - } else { - $payload['status'] = $resource->status; - } - - return $payload; - }); - } else { - $server->load(['settings']); - } - - return response()->json($server); - } -} diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php new file mode 100644 index 000000000..247a2519f --- /dev/null +++ b/app/Http/Controllers/Api/ServersController.php @@ -0,0 +1,396 @@ +user()->currentAccessToken(); + if ($token->can('view:sensitive')) { + return serializeApiResponse($settings); + } + $settings = $settings->makeHidden([ + 'metrics_token', + ]); + + return serializeApiResponse($settings); + } + + private function removeSensitiveData($server) + { + $token = auth()->user()->currentAccessToken(); + $server->makeHidden([ + 'id', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($server); + } + + return serializeApiResponse($server); + } + + #[OA\Get( + summary: 'List', + description: 'List all servers.', + path: '/servers', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all servers.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Server') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function servers(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) { + $server['is_reachable'] = $server->settings->is_reachable; + $server['is_usable'] = $server->settings->is_usable; + + return $server; + }); + $servers = $servers->map(function ($server) { + $settings = $this->removeSensitiveDataFromSettings($server->settings); + $server = $this->removeSensitiveData($server); + data_set($server, 'settings', $settings); + + return $server; + }); + + return response()->json($servers); + } + + #[OA\Get( + summary: 'Get', + description: 'Get server by UUID.', + path: '/servers/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get server by UUID', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + ref: '#/components/schemas/Server' + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function server_by_uuid(Request $request) + { + $with_resources = $request->query('resources'); + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); + if (is_null($server)) { + return response()->json(['message' => 'Server not found.'], 404); + } + if ($with_resources) { + $server['resources'] = $server->definedResources()->map(function ($resource) { + $payload = [ + 'id' => $resource->id, + 'uuid' => $resource->uuid, + 'name' => $resource->name, + 'type' => $resource->type(), + 'created_at' => $resource->created_at, + 'updated_at' => $resource->updated_at, + ]; + if ($resource->type() === 'service') { + $payload['status'] = $resource->status(); + } else { + $payload['status'] = $resource->status; + } + + return $payload; + }); + } else { + $server->load(['settings']); + } + + $settings = $this->removeSensitiveDataFromSettings($server->settings); + $server = $this->removeSensitiveData($server); + data_set($server, 'settings', $settings); + + return response()->json(serializeApiResponse($server)); + } + + #[OA\Get( + summary: 'Resources', + description: 'Get resources by server.', + path: '/servers/{uuid}/resources', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get resources by server', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items( + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'type' => ['type' => 'string'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + 'status' => ['type' => 'string'], + ] + ) + )), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function resources_by_server(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first(); + if (is_null($server)) { + return response()->json(['message' => 'Server not found.'], 404); + } + $server['resources'] = $server->definedResources()->map(function ($resource) { + $payload = [ + 'id' => $resource->id, + 'uuid' => $resource->uuid, + 'name' => $resource->name, + 'type' => $resource->type(), + 'created_at' => $resource->created_at, + 'updated_at' => $resource->updated_at, + ]; + if ($resource->type() === 'service') { + $payload['status'] = $resource->status(); + } else { + $payload['status'] = $resource->status; + } + + return $payload; + }); + $server = $this->removeSensitiveData($server); + ray($server); + + return response()->json(serializeApiResponse(data_get($server, 'resources'))); + } + + #[OA\Get( + summary: 'Domains', + description: 'Get domains by server.', + path: '/servers/{uuid}/domains', + security: [ + ['bearerAuth' => []], + ], + tags: ['Servers'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get domains by server', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items( + type: 'object', + properties: [ + 'ip' => ['type' => 'string'], + 'domains' => ['type' => 'array', 'items' => ['type' => 'string']], + ] + ) + )), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function domains_by_server(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->get('uuid'); + if ($uuid) { + $domains = Application::getDomainsByUuid($uuid); + + return response()->json(serializeApiResponse($domains)); + } + $projects = Project::where('team_id', $teamId)->get(); + $domains = collect(); + $applications = $projects->pluck('applications')->flatten(); + $settings = InstanceSettings::get(); + if ($applications->count() > 0) { + foreach ($applications as $application) { + $ip = $application->destination->server->ip; + $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { + $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/'); + + return str(str($f[0])->explode(':')[0]); + })->filter(function (Stringable $fqdn) { + return $fqdn->isNotEmpty(); + }); + + if ($ip === 'host.docker.internal') { + if ($settings->public_ipv4) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv4, + ]); + } + if ($settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv6, + ]); + } + if (! $settings->public_ipv4 && ! $settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } else { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } + } + $services = $projects->pluck('services')->flatten(); + if ($services->count() > 0) { + foreach ($services as $service) { + $service_applications = $service->applications; + if ($service_applications->count() > 0) { + foreach ($service_applications as $application) { + $fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) { + $f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/'); + + return str(str($f[0])->explode(':')[0]); + })->filter(function (Stringable $fqdn) { + return $fqdn->isNotEmpty(); + }); + if ($ip === 'host.docker.internal') { + if ($settings->public_ipv4) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv4, + ]); + } + if ($settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $settings->public_ipv6, + ]); + } + if (! $settings->public_ipv4 && ! $settings->public_ipv6) { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } else { + $domains->push([ + 'domain' => $fqdn, + 'ip' => $ip, + ]); + } + } + } + } + } + $domains = $domains->groupBy('ip')->map(function ($domain) { + return $domain->pluck('domain')->flatten(); + })->map(function ($domain, $ip) { + return [ + 'ip' => $ip, + 'domains' => $domain, + ]; + })->values(); + + return response()->json(serializeApiResponse($domains)); + } +} diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php new file mode 100644 index 000000000..7d58987d8 --- /dev/null +++ b/app/Http/Controllers/Api/ServicesController.php @@ -0,0 +1,702 @@ +user()->currentAccessToken(); + $service->makeHidden([ + 'id', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($service); + } + + $service->makeHidden([ + 'docker_compose_raw', + 'docker_compose', + ]); + + return serializeApiResponse($service); + } + + #[OA\Get( + summary: 'List', + description: 'List all services.', + path: '/services', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + responses: [ + new OA\Response( + response: 200, + description: 'Get all services', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Service') + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function services(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $projects = Project::where('team_id', $teamId)->get(); + $services = collect(); + foreach ($projects as $project) { + $services->push($project->services()->get()); + } + foreach ($services as $service) { + $service = $this->removeSensitiveData($service); + } + + return response()->json($services->flatten()); + } + + #[OA\Post( + summary: 'Create', + description: 'Create a one-click service', + path: '/services', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + requestBody: new OA\RequestBody( + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name', 'type'], + properties: [ + 'type' => [ + 'description' => 'The one-click service type', + 'type' => 'string', + 'enum' => [ + 'activepieces', + 'appsmith', + 'appwrite', + 'authentik', + 'babybuddy', + 'budge', + 'changedetection', + 'chatwoot', + 'classicpress-with-mariadb', + 'classicpress-with-mysql', + 'classicpress-without-database', + 'cloudflared', + 'code-server', + 'dashboard', + 'directus', + 'directus-with-postgresql', + 'docker-registry', + 'docuseal', + 'docuseal-with-postgres', + 'dokuwiki', + 'duplicati', + 'emby', + 'embystat', + 'fider', + 'filebrowser', + 'firefly', + 'formbricks', + 'ghost', + 'gitea', + 'gitea-with-mariadb', + 'gitea-with-mysql', + 'gitea-with-postgresql', + 'glance', + 'glances', + 'glitchtip', + 'grafana', + 'grafana-with-postgresql', + 'grocy', + 'heimdall', + 'homepage', + 'jellyfin', + 'kuzzle', + 'listmonk', + 'logto', + 'mediawiki', + 'meilisearch', + 'metabase', + 'metube', + 'minio', + 'moodle', + 'n8n', + 'n8n-with-postgresql', + 'next-image-transformation', + 'nextcloud', + 'nocodb', + 'odoo', + 'openblocks', + 'pairdrop', + 'penpot', + 'phpmyadmin', + 'pocketbase', + 'posthog', + 'reactive-resume', + 'rocketchat', + 'shlink', + 'slash', + 'snapdrop', + 'statusnook', + 'stirling-pdf', + 'supabase', + 'syncthing', + 'tolgee', + 'trigger', + 'trigger-with-external-database', + 'twenty', + 'umami', + 'unleash-with-postgresql', + 'unleash-without-database', + 'uptime-kuma', + 'vaultwarden', + 'vikunja', + 'weblate', + 'whoogle', + 'wordpress-with-mariadb', + 'wordpress-with-mysql', + 'wordpress-without-database', + ], + ], + 'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'], + 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'], + 'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'], + 'environment_name' => ['type' => 'string', 'description' => 'Environment name.'], + 'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'], + 'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'], + ], + ), + ), + ), + responses: [ + new OA\Response( + response: 201, + description: 'Create a service.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'uuid' => ['type' => 'string', 'description' => 'Service UUID.'], + 'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'], + ] + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function create_service(Request $request) + { + $allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy']; + + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + + $return = validateIncomingRequest($request); + if ($return instanceof \Illuminate\Http\JsonResponse) { + return $return; + } + $validator = customApiValidator($request->all(), [ + 'type' => 'string|required', + 'project_uuid' => 'string|required', + 'environment_name' => 'string|required', + 'server_uuid' => 'string|required', + 'destination_uuid' => 'string', + 'name' => 'string|max:255', + 'description' => 'string|nullable', + 'instant_deploy' => 'boolean', + ]); + + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + $serverUuid = $request->server_uuid; + $instantDeploy = $request->instant_deploy ?? false; + if ($request->is_public && ! $request->public_port) { + $request->offsetSet('is_public', false); + } + $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); + if (! $project) { + return response()->json(['message' => 'Project not found.'], 404); + } + $environment = $project->environments()->where('name', $request->environment_name)->first(); + if (! $environment) { + return response()->json(['message' => 'Environment not found.'], 404); + } + $server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found.'], 404); + } + $destinations = $server->destinations(); + if ($destinations->count() == 0) { + return response()->json(['message' => 'Server has no destinations.'], 400); + } + if ($destinations->count() > 1 && ! $request->has('destination_uuid')) { + return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400); + } + $destination = $destinations->first(); + $services = get_service_templates(); + $serviceKeys = $services->keys(); + if ($serviceKeys->contains($request->type)) { + $oneClickServiceName = $request->type; + $oneClickService = data_get($services, "$oneClickServiceName.compose"); + $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null); + if ($oneClickDotEnvs) { + $oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) { + return ! empty($value); + }); + } + if ($oneClickService) { + $service_payload = [ + 'name' => "$oneClickServiceName-".str()->random(10), + 'docker_compose_raw' => base64_decode($oneClickService), + 'environment_id' => $environment->id, + 'service_type' => $oneClickServiceName, + 'server_id' => $server->id, + 'destination_id' => $destination->id, + 'destination_type' => $destination->getMorphClass(), + ]; + if ($oneClickServiceName === 'cloudflared') { + data_set($service_payload, 'connect_to_docker_network', true); + } + $service = Service::create($service_payload); + $service->name = "$oneClickServiceName-".$service->uuid; + $service->save(); + if ($oneClickDotEnvs?->count() > 0) { + $oneClickDotEnvs->each(function ($value) use ($service) { + $key = str()->before($value, '='); + $value = str(str()->after($value, '=')); + $generatedValue = $value; + if ($value->contains('SERVICE_')) { + $command = $value->after('SERVICE_')->beforeLast('_'); + $generatedValue = generateEnvValue($command->value(), $service); + } + EnvironmentVariable::create([ + 'key' => $key, + 'value' => $generatedValue, + 'service_id' => $service->id, + 'is_build_time' => false, + 'is_preview' => false, + ]); + }); + } + $service->parse(isNew: true); + if ($instantDeploy) { + StartService::dispatch($service); + } + $domains = $service->applications()->get()->pluck('fqdn')->sort(); + $domains = $domains->map(function ($domain) { + return str($domain)->beforeLast(':')->value(); + }); + + return response()->json([ + 'uuid' => $service->uuid, + 'domains' => $domains, + ]); + } + + return response()->json(['message' => 'Service not found.'], 404); + } else { + return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400); + } + + return response()->json(['message' => 'Invalid service type.'], 400); + } + + #[OA\Get( + summary: 'Get', + description: 'Get service by UUID.', + path: '/services/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Get a service by Uuid.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + ref: '#/components/schemas/Service' + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function service_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + + return response()->json($this->removeSensitiveData($service)); + } + + #[OA\Delete( + summary: 'Delete', + description: 'Delete service by UUID.', + path: '/services/{uuid}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Delete a service by Uuid', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Service deletion request queued.'], + ], + ) + ), + ] + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function delete_by_uuid(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + if (! $request->uuid) { + return response()->json(['message' => 'UUID is required.'], 404); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + DeleteResourceJob::dispatch($service); + + return response()->json([ + 'message' => 'Service deletion request queued.', + ]); + } + + #[OA\Get( + summary: 'Start', + description: 'Start service. `Post` request is also accepted.', + path: '/services/{uuid}/start', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Start service.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Service starting request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_deploy(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + if (str($service->status())->contains('running')) { + return response()->json(['message' => 'Service is already running.'], 400); + } + StartService::dispatch($service); + + return response()->json( + [ + 'message' => 'Service starting request queued.', + ], + 200 + ); + } + + #[OA\Get( + summary: 'Stop', + description: 'Stop service. `Post` request is also accepted.', + path: '/services/{uuid}/stop', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Stop service.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_stop(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) { + return response()->json(['message' => 'Service is already stopped.'], 400); + } + StopService::dispatch($service); + + return response()->json( + [ + 'message' => 'Service stopping request queued.', + ], + 200 + ); + } + + #[OA\Get( + summary: 'Restart', + description: 'Restart service. `Post` request is also accepted.', + path: '/services/{uuid}/restart', + security: [ + ['bearerAuth' => []], + ], + tags: ['Services'], + parameters: [ + new OA\Parameter( + name: 'uuid', + in: 'path', + description: 'UUID of the service.', + required: true, + schema: new OA\Schema( + type: 'string', + format: 'uuid', + ) + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Restart service.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + properties: [ + 'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'], + ]) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function action_restart(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $uuid = $request->route('uuid'); + if (! $uuid) { + return response()->json(['message' => 'UUID is required.'], 400); + } + $service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first(); + if (! $service) { + return response()->json(['message' => 'Service not found.'], 404); + } + RestartService::dispatch($service); + + return response()->json( + [ + 'message' => 'Service restarting request queued.', + ], + 200 + ); + + } +} diff --git a/app/Http/Controllers/Api/Team.php b/app/Http/Controllers/Api/Team.php deleted file mode 100644 index c895f2c1b..000000000 --- a/app/Http/Controllers/Api/Team.php +++ /dev/null @@ -1,74 +0,0 @@ -user()->teams; - - return response()->json($teams); - } - - public function team_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404); - } - - return response()->json($team); - } - - public function members_by_id(Request $request) - { - $id = $request->id; - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $teams = auth()->user()->teams; - $team = $teams->where('id', $id)->first(); - if (is_null($team)) { - return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404); - } - - return response()->json($team->members); - } - - public function current_team(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team); - } - - public function current_team_members(Request $request) - { - $teamId = get_team_id_from_token(); - if (is_null($teamId)) { - return invalid_token(); - } - $team = auth()->user()->currentTeam(); - - return response()->json($team->members); - } -} diff --git a/app/Http/Controllers/Api/TeamController.php b/app/Http/Controllers/Api/TeamController.php new file mode 100644 index 000000000..1a481e5ec --- /dev/null +++ b/app/Http/Controllers/Api/TeamController.php @@ -0,0 +1,270 @@ +user()->currentAccessToken(); + $team->makeHidden([ + 'custom_server_limit', + 'pivot', + ]); + if ($token->can('view:sensitive')) { + return serializeApiResponse($team); + } + $team->makeHidden([ + 'smtp_username', + 'smtp_password', + 'resend_api_key', + 'telegram_token', + ]); + + return serializeApiResponse($team); + } + + #[OA\Get( + summary: 'List', + description: 'Get all teams.', + path: '/teams', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + responses: [ + new OA\Response( + response: 200, + description: 'List of teams.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Team') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function teams(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams->sortBy('id'); + $teams = $teams->map(function ($team) { + return $this->removeSensitiveData($team); + }); + + return response()->json( + $teams, + ); + } + + #[OA\Get( + summary: 'Get', + description: 'Get team by TeamId.', + path: '/teams/{id}', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + parameters: [ + new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of teams.', + content: new OA\JsonContent(ref: '#/components/schemas/Team') + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function team_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['message' => 'Team not found.'], 404); + } + $team = $this->removeSensitiveData($team); + + return response()->json( + serializeApiResponse($team), + ); + } + + #[OA\Get( + summary: 'Members', + description: 'Get members by TeamId.', + path: '/teams/{id}/members', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + parameters: [ + new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')), + ], + responses: [ + new OA\Response( + response: 200, + description: 'List of members.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/User') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 404, + ref: '#/components/responses/404', + ), + ] + )] + public function members_by_id(Request $request) + { + $id = $request->id; + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $teams = auth()->user()->teams; + $team = $teams->where('id', $id)->first(); + if (is_null($team)) { + return response()->json(['message' => 'Team not found.'], 404); + } + $members = $team->members; + $members->makeHidden([ + 'pivot', + ]); + + return response()->json( + serializeApiResponse($members), + ); + } + + #[OA\Get( + summary: 'Authenticated Team', + description: 'Get currently authenticated team.', + path: '/teams/current', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + responses: [ + new OA\Response( + response: 200, + description: 'Current Team.', + content: new OA\JsonContent(ref: '#/components/schemas/Team')), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function current_team(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + + return response()->json( + $this->removeSensitiveData($team), + ); + } + + #[OA\Get( + summary: 'Authenticated Team Members', + description: 'Get currently authenticated team members.', + path: '/teams/current/members', + security: [ + ['bearerAuth' => []], + ], + tags: ['Teams'], + responses: [ + new OA\Response( + response: 200, + description: 'Currently authenticated team members.', + content: [ + new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'array', + items: new OA\Items(ref: '#/components/schemas/User') + ) + ), + ]), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + ] + )] + public function current_team_members(Request $request) + { + $teamId = getTeamIdFromToken(); + if (is_null($teamId)) { + return invalidTokenResponse(); + } + $team = auth()->user()->currentTeam(); + $team->members->makeHidden([ + 'pivot', + ]); + + return response()->json( + serializeApiResponse($team->members), + ); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 3363d8164..38d9e2272 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -81,8 +81,8 @@ public function link() $token = request()->get('token'); if ($token) { $decrypted = Crypt::decryptString($token); - $email = Str::of($decrypted)->before('@@@'); - $password = Str::of($decrypted)->after('@@@'); + $email = str($decrypted)->before('@@@'); + $password = str($decrypted)->after('@@@'); $user = User::whereEmail($email)->first(); if (! $user) { return redirect()->route('login'); diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 5b17fe926..9569e8cfa 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -2,8 +2,10 @@ namespace App\Http\Controllers; +use App\Models\InstanceSettings; use App\Models\User; use Illuminate\Support\Facades\Auth; +use Symfony\Component\HttpKernel\Exception\HttpException; class OauthController extends Controller { @@ -20,6 +22,11 @@ public function callback(string $provider) $oauthUser = get_socialite_provider($provider)->user(); $user = User::whereEmail($oauthUser->email)->first(); if (! $user) { + $settings = InstanceSettings::get(); + if (! $settings->is_registration_enabled) { + abort(403, 'Registration is disabled'); + } + $user = User::create([ 'name' => $oauthUser->name, 'email' => $oauthUser->email, @@ -31,7 +38,9 @@ public function callback(string $provider) } catch (\Exception $e) { ray($e->getMessage()); - return redirect()->route('login')->withErrors([__('auth.failed.callback')]); + $errorCode = $e instanceof HttpException ? 'auth.failed' : 'auth.failed.callback'; + + return redirect()->route('login')->withErrors([__($errorCode)]); } } } diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index b9035b755..059438ff4 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -130,12 +130,23 @@ public function manual(Request $request) $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'bitbucket', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index 388481949..e6d91efd6 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -165,12 +165,24 @@ public function manual(Request $request) $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'gitea', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'gitea', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + } queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 403438193..a030e31ca 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -170,12 +170,23 @@ public function manual(Request $request) $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index a3d7712eb..f6e6cf7e7 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -180,12 +180,23 @@ public function manual(Request $request) $deployment_uuid = new Cuid2(7); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { - ApplicationPreview::create([ - 'git_type' => 'gitlab', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); + if ($application->build_pack === 'dockercompose') { + $pr_app = ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + 'docker_compose_domains' => $application->docker_compose_domains, + ]); + $pr_app->generate_preview_fqdn_compose(); + } else { + ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } } queue_application_deployment( application: $application, diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php index e404a8ebc..164322586 100644 --- a/app/Http/Controllers/Webhook/Stripe.php +++ b/app/Http/Controllers/Webhook/Stripe.php @@ -54,6 +54,34 @@ public function events(Request $request) $type = data_get($event, 'type'); $data = data_get($event, 'data.object'); switch ($type) { + case 'radar.early_fraud_warning.created': + $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); + $id = data_get($data, 'id'); + $charge = data_get($data, 'charge'); + if ($charge) { + $stripe->refunds->create(['charge' => $charge]); + } + $pi = data_get($data, 'payment_intent'); + $piData = $stripe->paymentIntents->retrieve($pi, []); + $customerId = data_get($piData, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if (! $subscription) { + Sleep::for(5)->seconds(); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + } + if (! $subscription) { + Sleep::for(5)->seconds(); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + } + if ($subscription) { + $subscriptionId = data_get($subscription, 'stripe_subscription_id'); + $stripe->subscriptions->cancel($subscriptionId, []); + $subscription->update([ + 'stripe_invoice_paid' => false, + ]); + } + send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}"); + break; case 'checkout.session.completed': $clientReferenceId = data_get($data, 'client_reference_id'); if (is_null($clientReferenceId)) { @@ -72,14 +100,14 @@ public function events(Request $request) } $subscription = Subscription::where('team_id', $teamId)->first(); if ($subscription) { - send_internal_notification('Old subscription activated for team: '.$teamId); + // send_internal_notification('Old subscription activated for team: '.$teamId); $subscription->update([ 'stripe_subscription_id' => $subscriptionId, 'stripe_customer_id' => $customerId, 'stripe_invoice_paid' => true, ]); } else { - send_internal_notification('New subscription for team: '.$teamId); + // send_internal_notification('New subscription for team: '.$teamId); Subscription::create([ 'team_id' => $teamId, 'stripe_subscription_id' => $subscriptionId, @@ -92,7 +120,7 @@ public function events(Request $request) $customerId = data_get($data, 'customer'); $planId = data_get($data, 'lines.data.0.plan.id'); if (Str::contains($excludedPlans, $planId)) { - send_internal_notification('Subscription excluded.'); + // send_internal_notification('Subscription excluded.'); break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); @@ -108,33 +136,33 @@ public function events(Request $request) $customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); if (! $subscription) { - send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); + // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId); return response('No subscription found in Coolify.'); } $team = data_get($subscription, 'team'); if (! $team) { - send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); + // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId); return response('No team found in Coolify.'); } if (! $subscription->stripe_invoice_paid) { SubscriptionInvoiceFailedJob::dispatch($team); - send_internal_notification('Invoice payment failed: '.$customerId); + // send_internal_notification('Invoice payment failed: '.$customerId); } else { - send_internal_notification('Invoice payment failed but already paid: '.$customerId); + // send_internal_notification('Invoice payment failed but already paid: '.$customerId); } break; case 'payment_intent.payment_failed': $customerId = data_get($data, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); if (! $subscription) { - send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); + // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId); return response('No subscription found in Coolify.'); } if ($subscription->stripe_invoice_paid) { - send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); + // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId); return; } @@ -146,7 +174,7 @@ public function events(Request $request) $subscriptionId = data_get($data, 'items.data.0.subscription'); $planId = data_get($data, 'items.data.0.plan.id'); if (Str::contains($excludedPlans, $planId)) { - send_internal_notification('Subscription excluded.'); + // send_internal_notification('Subscription excluded.'); break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); @@ -156,11 +184,11 @@ public function events(Request $request) } if (! $subscription) { if ($status === 'incomplete_expired') { - send_internal_notification('Subscription incomplete expired for customer: '.$customerId); + // send_internal_notification('Subscription incomplete expired for customer: '.$customerId); return response('Subscription incomplete expired', 200); } - send_internal_notification('No subscription found for: '.$customerId); + // send_internal_notification('No subscription found for: '.$customerId); return response('No subscription found', 400); } @@ -194,7 +222,7 @@ public function events(Request $request) $subscription->update([ 'stripe_invoice_paid' => false, ]); - send_internal_notification('Subscription paused or incomplete for customer: '.$customerId); + // send_internal_notification('Subscription paused or incomplete for customer: '.$customerId); } // Trial ended but subscribed, reactive servers @@ -208,13 +236,13 @@ public function events(Request $request) if ($comment) { $reason .= ' with comment: \''.$comment."'"; } - send_internal_notification($reason); + // send_internal_notification($reason); } if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) { if ($cancelAtPeriodEnd) { // send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id); } else { - send_internal_notification('customer.subscription.updated for customer: '.$customerId); + // send_internal_notification('customer.subscription.updated for customer: '.$customerId); } } break; @@ -231,9 +259,9 @@ public function events(Request $request) 'stripe_plan_id' => null, 'stripe_cancel_at_period_end' => false, 'stripe_invoice_paid' => false, - 'stripe_trial_already_ended' => true, + 'stripe_trial_already_ended' => false, ]); - send_internal_notification('customer.subscription.deleted for customer: '.$customerId); + // send_internal_notification('customer.subscription.deleted for customer: '.$customerId); break; case 'customer.subscription.trial_will_end': // Not used for now @@ -258,7 +286,7 @@ public function events(Request $request) 'stripe_invoice_paid' => false, ]); SubscriptionTrialEndedJob::dispatch($team); - send_internal_notification('Subscription paused for customer: '.$customerId); + // send_internal_notification('Subscription paused for customer: '.$customerId); break; default: // Unhandled event type diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index e29c4a307..5f1731071 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -67,5 +67,7 @@ class Kernel extends HttpKernel 'signed' => \App\Http\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + 'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class, + 'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class, ]; } diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php new file mode 100644 index 000000000..dc0a433e2 --- /dev/null +++ b/app/Http/Middleware/ApiAllowed.php @@ -0,0 +1,34 @@ +clearAll(); + if (isCloud()) { + return $next($request); + } + $settings = InstanceSettings::get(); + if ($settings->is_api_enabled === false) { + return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); + } + + if (! isDev()) { + if ($settings->allowed_ips) { + $allowedIps = explode(',', $settings->allowed_ips); + if (! in_array($request->ip(), $allowedIps)) { + return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403); + } + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/IgnoreReadOnlyApiToken.php b/app/Http/Middleware/IgnoreReadOnlyApiToken.php new file mode 100644 index 000000000..bd6cd1f8a --- /dev/null +++ b/app/Http/Middleware/IgnoreReadOnlyApiToken.php @@ -0,0 +1,28 @@ +user()->currentAccessToken(); + if ($token->can('*')) { + return $next($request); + } + if ($token->can('read-only')) { + return response()->json(['message' => 'You are not allowed to perform this action.'], 403); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/OnlyRootApiToken.php b/app/Http/Middleware/OnlyRootApiToken.php new file mode 100644 index 000000000..8ff1fa0e5 --- /dev/null +++ b/app/Http/Middleware/OnlyRootApiToken.php @@ -0,0 +1,25 @@ +user()->currentAccessToken(); + if ($token->can('*')) { + return $next($request); + } + + return response()->json(['message' => 'You are not allowed to perform this action.'], 403); + } +} diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 72d8c0ad1..f0f9ac0af 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -9,6 +9,7 @@ use App\Models\Application; use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationPreview; +use App\Models\EnvironmentVariable; use App\Models\GithubApp; use App\Models\GitlabApp; use App\Models\Server; @@ -126,7 +127,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private string $dockerfile_location = '/Dockerfile'; - private string $docker_compose_location = '/docker-compose.yml'; + private string $docker_compose_location = '/docker-compose.yaml'; private ?string $docker_compose_custom_start_command = null; @@ -193,6 +194,9 @@ public function __construct(int $application_deployment_queue_id) $this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id); + if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) { + $this->container_name = $this->application->settings->custom_internal_name; + } ray('New container name: ', $this->container_name); savePrivateKeyToFs($this->server); @@ -339,7 +343,7 @@ private function decide_what_to_do() private function post_deployment() { if ($this->server->isProxyShouldRun()) { - GetContainersStatus::dispatch($this->server); + GetContainersStatus::dispatch($this->server)->onQueue('high'); // dispatch(new ContainerStatusJob($this->server)); } $this->next(ApplicationDeploymentStatus::FINISHED->value); @@ -607,10 +611,10 @@ private function write_deployment_configurations() } $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); if ($this->pull_request_id === 0) { - $composeFileName = "$this->configuration_dir/docker-compose.yml"; + $composeFileName = "$this->configuration_dir/docker-compose.yaml"; } else { - $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml"; - $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yml"; + $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yaml"; + $this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml"; } $this->execute_remote_command( [ @@ -827,6 +831,9 @@ private function save_environment_variables() if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) { $envs->push("COOLIFY_BRANCH={$local_branch}"); } + if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + } foreach ($sorted_environment_variables_preview as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { @@ -868,6 +875,9 @@ private function save_environment_variables() if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) { $envs->push("COOLIFY_BRANCH={$local_branch}"); } + if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) { + $envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}"); + } foreach ($sorted_environment_variables as $env) { $real_value = $env->real_value; if ($env->version === '4.0.0-beta.239') { @@ -877,7 +887,6 @@ private function save_environment_variables() $real_value = '\''.$real_value.'\''; } else { $real_value = escapeEnvVariables($env->real_value); - ray($real_value); } } $envs->push($env->key.'='.$real_value); @@ -946,9 +955,8 @@ private function save_environment_variables() } } - private function framework_based_notification() + private function laravel_finetunes() { - // Laravel old env variables if ($this->pull_request_id === 0) { $nixpacks_php_fallback_path = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); $nixpacks_php_root_dir = $this->application->environment_variables->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); @@ -956,9 +964,24 @@ private function framework_based_notification() $nixpacks_php_fallback_path = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_FALLBACK_PATH')->first(); $nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first(); } - if ($nixpacks_php_fallback_path?->value === '/index.php' && $nixpacks_php_root_dir?->value === '/app/public' && $this->newVersionIsHealthy === false) { - $this->application_deployment_queue->addLogEntry('There was a change in how Laravel is deployed. Please update your environment variables to match the new deployment method. More details here: https://coolify.io/docs/resources/laravel', 'stderr'); + if (! $nixpacks_php_fallback_path) { + $nixpacks_php_fallback_path = new EnvironmentVariable(); + $nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH'; + $nixpacks_php_fallback_path->value = '/index.php'; + $nixpacks_php_fallback_path->is_build_time = false; + $nixpacks_php_fallback_path->application_id = $this->application->id; + $nixpacks_php_fallback_path->save(); } + if (! $nixpacks_php_root_dir) { + $nixpacks_php_root_dir = new EnvironmentVariable(); + $nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR'; + $nixpacks_php_root_dir->value = '/app/public'; + $nixpacks_php_root_dir->is_build_time = false; + $nixpacks_php_root_dir->application_id = $this->application->id; + $nixpacks_php_root_dir->save(); + } + + return [$nixpacks_php_fallback_path, $nixpacks_php_root_dir]; } private function rolling_update() @@ -1005,7 +1028,6 @@ private function rolling_update() $this->application_deployment_queue->addLogEntry('Rolling update completed.'); } } - $this->framework_based_notification(); } private function health_check() @@ -1059,13 +1081,13 @@ private function health_check() $this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}"); } - if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') { + if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') { $this->newVersionIsHealthy = true; $this->application->update(['status' => 'running']); $this->application_deployment_queue->addLogEntry('New container is healthy.'); break; } - if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') { + if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') { $this->newVersionIsHealthy = false; $this->query_logs(); break; @@ -1077,7 +1099,7 @@ private function health_check() $sleeptime++; } } - if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') { + if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') { $this->query_logs(); } } @@ -1366,17 +1388,20 @@ private function generate_nixpacks_confs() throw new RuntimeException('Nixpacks failed to detect the application type. Please check the documentation of Nixpacks: https://nixpacks.com/docs/providers'); } } + if ($this->saved_outputs->get('nixpacks_plan')) { $this->nixpacks_plan = $this->saved_outputs->get('nixpacks_plan'); if ($this->nixpacks_plan) { $this->application_deployment_queue->addLogEntry("Found application type: {$this->nixpacks_type}."); $this->application_deployment_queue->addLogEntry("If you need further customization, please check the documentation of Nixpacks: https://nixpacks.com/docs/providers/{$this->nixpacks_type}"); $parsed = Toml::Parse($this->nixpacks_plan); + // Do any modifications here $this->generate_env_variables(); $merged_envs = $this->env_args->merge(collect(data_get($parsed, 'variables', []))); $aptPkgs = data_get($parsed, 'phases.setup.aptPkgs', []); if (count($aptPkgs) === 0) { + $aptPkgs = ['curl', 'wget']; data_set($parsed, 'phases.setup.aptPkgs', ['curl', 'wget']); } else { if (! in_array('curl', $aptPkgs)) { @@ -1388,6 +1413,12 @@ private function generate_nixpacks_confs() data_set($parsed, 'phases.setup.aptPkgs', $aptPkgs); } data_set($parsed, 'variables', $merged_envs->toArray()); + $is_laravel = data_get($parsed, 'variables.IS_LARAVEL', false); + if ($is_laravel) { + $variables = $this->laravel_finetunes(); + data_set($parsed, 'variables.NIXPACKS_PHP_FALLBACK_PATH', $variables[0]->value); + data_set($parsed, 'variables.NIXPACKS_PHP_ROOT_DIR', $variables[1]->value); + } $this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT); $this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true); } @@ -1509,7 +1540,7 @@ private function generate_compose_file() $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true, ]); - $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n")); + $dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n")); $this->application->parseHealthcheckFromDockerfile($dockerfile); } $docker_compose = [ @@ -1542,23 +1573,6 @@ private function generate_compose_file() ], ], ]; - if (isset($this->application->settings->custom_internal_name)) { - $docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name; - } - // if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) { - // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) { - // $docker_compose['services'][$this->container_name]['env_file'][] = '.env'; - // } else { - // $docker_compose['services'][$this->container_name]['env_file'] = ['.env']; - // } - // } - // if ($this->env_filename) { - // if (data_get($docker_compose, "services.{$this->container_name}.env_file")) { - // $docker_compose['services'][$this->container_name]['env_file'][] = $this->env_filename; - // } else { - // $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; - // } - // } if (! is_null($this->env_filename)) { $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename]; } @@ -1669,32 +1683,28 @@ private function generate_compose_file() if (count($volume_names) > 0) { $docker_compose['volumes'] = $volume_names; } - // if ($this->build_pack === 'dockerfile') { - // $docker_compose['services'][$this->container_name]['build'] = [ - // 'context' => $this->workdir, - // 'dockerfile' => $this->workdir . $this->dockerfile_location, - // ]; - // } if ($this->pull_request_id === 0) { $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); if ((bool) $this->application->settings->is_consistent_container_name_enabled) { - $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; - if (count($custom_compose) > 0) { - $ipv4 = data_get($custom_compose, 'ip.0'); - $ipv6 = data_get($custom_compose, 'ip6.0'); - data_forget($custom_compose, 'ip'); - data_forget($custom_compose, 'ip6'); - if ($ipv4 || $ipv6) { - data_forget($docker_compose['services'][$this->application->uuid], 'networks'); + if (! $this->application->settings->custom_internal_name) { + $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; + if (count($custom_compose) > 0) { + $ipv4 = data_get($custom_compose, 'ip.0'); + $ipv6 = data_get($custom_compose, 'ip6.0'); + data_forget($custom_compose, 'ip'); + data_forget($custom_compose, 'ip6'); + if ($ipv4 || $ipv6) { + data_forget($docker_compose['services'][$this->application->uuid], 'networks'); + } + if ($ipv4) { + $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; + } + if ($ipv6) { + $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6; + } + $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); } - if ($ipv4) { - $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4; - } - if ($ipv6) { - $docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6; - } - $docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose); } } else { if (count($custom_compose) > 0) { @@ -1718,7 +1728,7 @@ private function generate_compose_file() $this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose_base64 = base64_encode($this->docker_compose); - $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yml > /dev/null"), 'hidden' => true]); + $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yaml > /dev/null"), 'hidden' => true]); } private function generate_local_persistent_volumes() @@ -1841,13 +1851,25 @@ private function build_image() $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); if ($this->force_rebuild) { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->build_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->build_image_name} {$this->workdir}"; } + + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, + ] + ); $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { if ($this->force_rebuild) { @@ -1866,7 +1888,6 @@ private function build_image() ] ); } - $dockerfile = base64_encode("FROM {$this->application->static_image} WORKDIR /usr/share/nginx/html/ LABEL coolify.deploymentId={$this->deployment_uuid} @@ -1929,13 +1950,24 @@ private function build_image() $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->nixpacks_plan}' | base64 -d | tee /artifacts/thegameplan.json > /dev/null"), 'hidden' => true]); if ($this->force_rebuild) { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --no-cache --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build --no-cache {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } else { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir}"), 'hidden' => true, + executeInDocker($this->deployment_uuid, "nixpacks build -c /artifacts/thegameplan.json --cache-key '{$this->application->uuid}' --no-error-without-start -n {$this->production_image_name} {$this->workdir} -o {$this->workdir}"), 'hidden' => true, ]); + $build_command = "docker build {$this->addHosts} --network host -f {$this->workdir}/.nixpacks/Dockerfile {$this->build_args} --progress plain -t {$this->production_image_name} {$this->workdir}"; } + $base64_build_command = base64_encode($build_command); + $this->execute_remote_command( + [ + executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"), 'hidden' => true, + ], + [ + executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'), 'hidden' => true, + ] + ); $this->execute_remote_command([executeInDocker($this->deployment_uuid, 'rm /artifacts/thegameplan.json'), 'hidden' => true]); } else { if ($this->force_rebuild) { @@ -2059,7 +2091,7 @@ private function add_build_env_variables_to_dockerfile() $this->execute_remote_command([ executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile', ]); - $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); if ($this->pull_request_id === 0) { foreach ($this->application->build_environment_variables as $env) { if (data_get($env, 'is_multiline') === true) { @@ -2184,10 +2216,14 @@ public function failed(Throwable $exception): void ray($code); if ($code !== 69420) { // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one - $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); - $this->execute_remote_command( - ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true] - ); + if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) { + // do not remove already running container + } else { + $this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr'); + $this->execute_remote_command( + ["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true] + ); + } } } } diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php index d400642dd..6120d1cba 100755 --- a/app/Jobs/ApplicationPullRequestUpdateJob.php +++ b/app/Jobs/ApplicationPullRequestUpdateJob.php @@ -25,8 +25,7 @@ public function __construct( public ApplicationPreview $preview, public ProcessStatus $status, public ?string $deployment_uuid = null - ) { - } + ) {} public function handle() { diff --git a/app/Jobs/CheckLogDrainContainerJob.php b/app/Jobs/CheckLogDrainContainerJob.php index 312200f66..16ef85192 100644 --- a/app/Jobs/CheckLogDrainContainerJob.php +++ b/app/Jobs/CheckLogDrainContainerJob.php @@ -19,9 +19,7 @@ class CheckLogDrainContainerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function middleware(): array { diff --git a/app/Jobs/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php index 8f2039ef2..b55ae9967 100644 --- a/app/Jobs/CheckResaleLicenseJob.php +++ b/app/Jobs/CheckResaleLicenseJob.php @@ -14,9 +14,7 @@ class CheckResaleLicenseJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct() - { - } + public function __construct() {} public function handle(): void { diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php index 418c7a0f4..7b064a464 100644 --- a/app/Jobs/CleanupHelperContainersJob.php +++ b/app/Jobs/CleanupHelperContainersJob.php @@ -15,9 +15,7 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function handle(): void { diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index b846ad2bc..d9de3f6fe 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -16,10 +16,7 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct() - { - - } + public function __construct() {} // public function uniqueId(): string // { diff --git a/app/Jobs/ContainerStatusJob.php b/app/Jobs/ContainerStatusJob.php index c50d17d4c..e919855d5 100644 --- a/app/Jobs/ContainerStatusJob.php +++ b/app/Jobs/ContainerStatusJob.php @@ -23,9 +23,7 @@ public function backoff(): int return isDev() ? 1 : 3; } - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function middleware(): array { diff --git a/app/Jobs/CoolifyTask.php b/app/Jobs/CoolifyTask.php index e5f4dfd5e..c3692c30b 100755 --- a/app/Jobs/CoolifyTask.php +++ b/app/Jobs/CoolifyTask.php @@ -20,11 +20,10 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue */ public function __construct( public Activity $activity, - public bool $ignore_errors = false, - public $call_event_on_finish = null, - public $call_event_data = null - ) { - } + public bool $ignore_errors, + public $call_event_on_finish, + public $call_event_data, + ) {} /** * Execute the job. diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 07386988c..4afe50d53 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -98,7 +98,7 @@ public function handle(): void return; } - $status = Str::of(data_get($this->database, 'status')); + $status = str(data_get($this->database, 'status')); if (! $status->startsWith('running') && $this->database->id !== 0) { ray('database not running'); @@ -236,7 +236,7 @@ public function handle(): void return; } } - $this->backup_dir = backup_dir().'/databases/'.Str::of($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; + $this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; if ($this->database->name === 'coolify-db') { $databasesToBackup = ['coolify']; @@ -332,8 +332,7 @@ public function handle(): void private function backup_standalone_mongodb(string $databaseWithCollections): void { try { - ray($this->database->toArray()); - $url = $this->database->get_db_url(useInternal: true); + $url = $this->database->internal_db_url; if ($databaseWithCollections === 'all') { $commands[] = 'mkdir -p '.$this->backup_dir; if (str($this->database->image)->startsWith('mongo:4.0')) { diff --git a/app/Jobs/DatabaseBackupStatusJob.php b/app/Jobs/DatabaseBackupStatusJob.php index cf240e0d7..d3b0e99cf 100644 --- a/app/Jobs/DatabaseBackupStatusJob.php +++ b/app/Jobs/DatabaseBackupStatusJob.php @@ -18,9 +18,7 @@ class DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue public $tries = 1; - public function __construct() - { - } + public function __construct() {} public function handle() { diff --git a/app/Jobs/DeleteResourceJob.php b/app/Jobs/DeleteResourceJob.php index 6d4720f6b..8710fda88 100644 --- a/app/Jobs/DeleteResourceJob.php +++ b/app/Jobs/DeleteResourceJob.php @@ -28,9 +28,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false) - { - } + public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false) {} public function handle() { diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 32c41e99c..785940ee6 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -22,9 +22,7 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public ?int $usageBefore = null; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function handle(): void { @@ -37,9 +35,9 @@ public function handle(): void return; } }); - if ($isInprogress) { - throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); - } + // if ($isInprogress) { + // throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); + // } if (! $this->server->isFunctional()) { return; } @@ -62,7 +60,7 @@ public function handle(): void Log::info('No need to clean up '.$this->server->name); } } catch (\Throwable $e) { - send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage()); + // send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php index bab8f3a25..3188d35d6 100644 --- a/app/Jobs/GithubAppPermissionJob.php +++ b/app/Jobs/GithubAppPermissionJob.php @@ -23,9 +23,7 @@ public function backoff(): int return isDev() ? 1 : 3; } - public function __construct(public GithubApp $github_app) - { - } + public function __construct(public GithubApp $github_app) {} public function middleware(): array { diff --git a/app/Jobs/InstanceAutoUpdateJob.php b/app/Jobs/InstanceAutoUpdateJob.php index bce60bbc8..1bbfcf8cb 100644 --- a/app/Jobs/InstanceAutoUpdateJob.php +++ b/app/Jobs/InstanceAutoUpdateJob.php @@ -19,9 +19,7 @@ class InstanceAutoUpdateJob implements ShouldBeEncrypted, ShouldBeUnique, Should public $tries = 1; - public function __construct() - { - } + public function __construct() {} public function handle(): void { diff --git a/app/Jobs/PullCoolifyImageJob.php b/app/Jobs/PullCoolifyImageJob.php index ccaa785dc..2bcbfc4df 100644 --- a/app/Jobs/PullCoolifyImageJob.php +++ b/app/Jobs/PullCoolifyImageJob.php @@ -19,9 +19,7 @@ class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 1000; - public function __construct() - { - } + public function __construct() {} public function handle(): void { diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index d3bda2ea1..30a1b8026 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -27,9 +27,7 @@ public function uniqueId(): string return $this->server->uuid; } - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function handle(): void { diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php index 1dd4b1dd3..f8c769382 100644 --- a/app/Jobs/PullSentinelImageJob.php +++ b/app/Jobs/PullSentinelImageJob.php @@ -28,9 +28,7 @@ public function uniqueId(): string return $this->server->uuid; } - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function handle(): void { @@ -52,7 +50,7 @@ public function handle(): void } ray('Sentinel image is up to date'); } catch (\Throwable $e) { - send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); + // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); ray($e->getMessage()); throw $e; } diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php index 948060033..396ff29f4 100644 --- a/app/Jobs/PullTemplatesFromCDN.php +++ b/app/Jobs/PullTemplatesFromCDN.php @@ -17,9 +17,7 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue public $timeout = 10; - public function __construct() - { - } + public function __construct() {} public function handle(): void { diff --git a/app/Jobs/PullVersionsFromCDN.php b/app/Jobs/PullVersionsFromCDN.php index 1ad4989de..79ebad7a8 100644 --- a/app/Jobs/PullVersionsFromCDN.php +++ b/app/Jobs/PullVersionsFromCDN.php @@ -17,9 +17,7 @@ class PullVersionsFromCDN implements ShouldBeEncrypted, ShouldQueue public $timeout = 10; - public function __construct() - { - } + public function __construct() {} public function handle(): void { diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php index 4d5618df0..73e8658ee 100755 --- a/app/Jobs/SendConfirmationForWaitlistJob.php +++ b/app/Jobs/SendConfirmationForWaitlistJob.php @@ -14,9 +14,7 @@ class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public string $email, public string $uuid) - { - } + public function __construct(public string $email, public string $uuid) {} public function handle() { diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index 90f2e0b30..f38cf823c 100644 --- a/app/Jobs/SendMessageToDiscordJob.php +++ b/app/Jobs/SendMessageToDiscordJob.php @@ -31,8 +31,7 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue public function __construct( public string $text, public string $webhookUrl - ) { - } + ) {} /** * Execute the job. diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index b81bbc50b..bf52b782f 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -33,8 +33,7 @@ public function __construct( public string $token, public string $chatId, public ?string $topicId = null, - ) { - } + ) {} /** * Execute the job. diff --git a/app/Jobs/ServerFilesFromServerJob.php b/app/Jobs/ServerFilesFromServerJob.php index 2476c12dd..769dfc004 100644 --- a/app/Jobs/ServerFilesFromServerJob.php +++ b/app/Jobs/ServerFilesFromServerJob.php @@ -16,9 +16,7 @@ class ServerFilesFromServerJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) - { - } + public function __construct(public ServiceApplication|ServiceDatabase|Application $resource) {} public function handle() { diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index 3eaf88ba7..24292025b 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -24,9 +24,7 @@ public function backoff(): int return isDev() ? 1 : 3; } - public function __construct(public Team $team) - { - } + public function __construct(public Team $team) {} public function middleware(): array { diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php index aaf8f5784..bddafe2ba 100644 --- a/app/Jobs/ServerStatusJob.php +++ b/app/Jobs/ServerStatusJob.php @@ -25,9 +25,7 @@ public function backoff(): int return isDev() ? 1 : 3; } - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function middleware(): array { @@ -48,12 +46,12 @@ public function handle() if ($this->server->isFunctional()) { $this->cleanup(notify: false); $this->remove_unnecessary_coolify_yaml(); - if (config('coolify.is_sentinel_enabled')) { + if ($this->server->isSentinelEnabled()) { $this->server->checkSentinel(); } } } catch (\Throwable $e) { - send_internal_notification('ServerStatusJob failed with: '.$e->getMessage()); + // send_internal_notification('ServerStatusJob failed with: '.$e->getMessage()); ray($e->getMessage()); return handleError($e); diff --git a/app/Jobs/ServerStorageSaveJob.php b/app/Jobs/ServerStorageSaveJob.php index c94a3edc5..526cd5375 100644 --- a/app/Jobs/ServerStorageSaveJob.php +++ b/app/Jobs/ServerStorageSaveJob.php @@ -14,9 +14,7 @@ class ServerStorageSaveJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(public LocalFileVolume $localFileVolume) - { - } + public function __construct(public LocalFileVolume $localFileVolume) {} public function handle() { diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php index e4cd219c8..64a75671f 100755 --- a/app/Jobs/SubscriptionInvoiceFailedJob.php +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -15,9 +15,7 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public function __construct(protected Team $team) - { - } + public function __construct(protected Team $team) {} public function handle() { diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php index ee260d8d9..dd2250dd7 100755 --- a/app/Jobs/SubscriptionTrialEndedJob.php +++ b/app/Jobs/SubscriptionTrialEndedJob.php @@ -17,8 +17,7 @@ class SubscriptionTrialEndedJob implements ShouldBeEncrypted, ShouldQueue public function __construct( public Team $team - ) { - } + ) {} public function handle(): void { diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php index fba668108..80e232a3e 100755 --- a/app/Jobs/SubscriptionTrialEndsSoonJob.php +++ b/app/Jobs/SubscriptionTrialEndsSoonJob.php @@ -17,8 +17,7 @@ class SubscriptionTrialEndsSoonJob implements ShouldBeEncrypted, ShouldQueue public function __construct( public Team $team - ) { - } + ) {} public function handle(): void { diff --git a/app/Listeners/MaintenanceModeDisabledNotification.php b/app/Listeners/MaintenanceModeDisabledNotification.php index 9f676ca99..ded53ccee 100644 --- a/app/Listeners/MaintenanceModeDisabledNotification.php +++ b/app/Listeners/MaintenanceModeDisabledNotification.php @@ -9,9 +9,7 @@ class MaintenanceModeDisabledNotification { - public function __construct() - { - } + public function __construct() {} public function handle(EventsMaintenanceModeDisabled $event): void { diff --git a/app/Listeners/ProxyStartedNotification.php b/app/Listeners/ProxyStartedNotification.php index 64271cc52..d0541b162 100644 --- a/app/Listeners/ProxyStartedNotification.php +++ b/app/Listeners/ProxyStartedNotification.php @@ -9,9 +9,7 @@ class ProxyStartedNotification { public Server $server; - public function __construct() - { - } + public function __construct() {} public function handle(ProxyStarted $event): void { diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index b787ed0cc..7acf5ed87 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -12,7 +12,7 @@ class Index extends Component { - protected $listeners = ['serverInstalled' => 'validateServer']; + protected $listeners = ['refreshBoardingIndex' => 'validateServer']; public string $currentState = 'welcome'; diff --git a/app/Livewire/MonacoEditor.php b/app/Livewire/MonacoEditor.php new file mode 100644 index 000000000..156c63d3a --- /dev/null +++ b/app/Livewire/MonacoEditor.php @@ -0,0 +1,51 @@ + '$refresh', + ]; + + public function __construct( + public ?string $id, + public ?string $name, + public ?string $type, + public ?string $monacoContent, + public ?string $value, + public ?string $label, + public ?string $placeholder, + public bool $required, + public bool $disabled, + public bool $readonly, + public bool $allowTab, + public bool $spellcheck, + public ?string $helper, + public bool $realtimeValidation, + public bool $allowToPeak, + public string $defaultClass, + public string $defaultClassInput, + public ?string $language + + ) { + // + } + + public function render() + { + if (is_null($this->id)) { + $this->id = new Cuid2(7); + } + + if (is_null($this->name)) { + $this->name = $this->id; + } + + return view('components.forms.monaco-editor'); + } +} diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index cbbe98d99..b3e39d23d 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -54,9 +54,9 @@ public function force_start() public function cancel() { + $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; + $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; try { - $kill_command = "docker rm -f {$this->application_deployment_queue->deployment_uuid}"; - $server_id = $this->application_deployment_queue->server_id ?? $this->application->destination->server_id; $server = Server::find($server_id); if ($this->application_deployment_queue->logs) { $previous_logs = json_decode($this->application_deployment_queue->logs, associative: true, flags: JSON_THROW_ON_ERROR); @@ -84,6 +84,7 @@ public function cancel() 'current_process_id' => null, 'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value, ]); + next_after_cancel($server); } } } diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 60cdee48e..48be89714 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -5,7 +5,6 @@ use App\Models\Application; use App\Models\LocalFileVolume; use Illuminate\Support\Collection; -use Illuminate\Support\Str; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -41,8 +40,6 @@ class General extends Component public ?string $initialDockerComposeLocation = null; - public ?string $initialDockerComposePrLocation = null; - public ?Collection $parsedServices; public $parsedServiceDomains = []; @@ -73,11 +70,8 @@ class General extends Component 'application.docker_registry_image_tag' => 'nullable', 'application.dockerfile_location' => 'nullable', 'application.docker_compose_location' => 'nullable', - 'application.docker_compose_pr_location' => 'nullable', 'application.docker_compose' => 'nullable', - 'application.docker_compose_pr' => 'nullable', 'application.docker_compose_raw' => 'nullable', - 'application.docker_compose_pr_raw' => 'nullable', 'application.dockerfile_target_build' => 'nullable', 'application.docker_compose_custom_start_command' => 'nullable', 'application.docker_compose_custom_build_command' => 'nullable', @@ -115,11 +109,8 @@ class General extends Component 'application.docker_registry_image_tag' => 'Docker registry image tag', 'application.dockerfile_location' => 'Dockerfile location', 'application.docker_compose_location' => 'Docker compose location', - 'application.docker_compose_pr_location' => 'Docker compose location', 'application.docker_compose' => 'Docker compose', - 'application.docker_compose_pr' => 'Docker compose', 'application.docker_compose_raw' => 'Docker compose raw', - 'application.docker_compose_pr_raw' => 'Docker compose raw', 'application.custom_labels' => 'Custom labels', 'application.dockerfile_target_build' => 'Dockerfile target build', 'application.custom_docker_run_options' => 'Custom docker run commands', @@ -184,7 +175,7 @@ public function loadComposeFile($isInit = false) if ($isInit && $this->application->docker_compose_raw) { return; } - ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $this->application->loadComposeFile($isInit); + ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation] = $this->application->loadComposeFile($isInit); if (is_null($this->parsedServices)) { $this->dispatch('error', 'Failed to parse your docker-compose file. Please check the syntax and try again.'); @@ -199,8 +190,8 @@ public function loadComposeFile($isInit = false) return str($volume)->startsWith('/data/coolify'); })->unique()->values(); foreach ($volumes as $volume) { - $source = Str::of($volume)->before(':'); - $target = Str::of($volume)->after(':')->beforeLast(':'); + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); LocalFileVolume::updateOrCreate( [ @@ -223,7 +214,6 @@ public function loadComposeFile($isInit = false) $this->dispatch('refreshEnvs'); } catch (\Throwable $e) { $this->application->docker_compose_location = $this->initialDockerComposeLocation; - $this->application->docker_compose_pr_location = $this->initialDockerComposePrLocation; $this->application->save(); return handleError($e, $this); @@ -347,7 +337,9 @@ public function set_redirect() public function submit($showToaster = true) { try { - $this->set_redirect(); + if ($this->application->isDirty('redirect')) { + $this->set_redirect(); + } $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php index d224f4a9d..feb54c7f0 100644 --- a/app/Livewire/Project/Application/Heading.php +++ b/app/Livewire/Project/Application/Heading.php @@ -45,7 +45,7 @@ public function mount() public function check_status($showNotification = false) { if ($this->application->destination->server->isFunctional()) { - GetContainersStatus::dispatch($this->application->destination->server); + GetContainersStatus::dispatch($this->application->destination->server)->onQueue('high'); // dispatch(new ContainerStatusJob($this->application->destination->server)); } else { dispatch(new ServerStatusJob($this->application->destination->server)); diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php index cf5ab9c82..e4f100fcf 100644 --- a/app/Livewire/Project/Application/Preview/Form.php +++ b/app/Livewire/Project/Application/Preview/Form.php @@ -3,7 +3,6 @@ namespace App\Livewire\Project\Application\Preview; use App\Models\Application; -use Illuminate\Support\Str; use Livewire\Component; use Spatie\Url\Url; @@ -32,10 +31,10 @@ public function resetToDefault() public function generate_real_url() { if (data_get($this->application, 'fqdn')) { - $firstFqdn = Str::of($this->application->fqdn)->before(','); + $firstFqdn = str($this->application->fqdn)->before(','); $url = Url::fromString($firstFqdn); $host = $url->getHost(); - $this->preview_url_template = Str::of($this->application->preview_url_template)->replace('{{domain}}', $host); + $this->preview_url_template = str($this->application->preview_url_template)->replace('{{domain}}', $host); } } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index ca911339e..df64c3fd3 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -131,6 +131,12 @@ public function add(int $pull_request_id, ?string $pull_request_html_url = null) } } + public function add_and_deploy(int $pull_request_id, ?string $pull_request_html_url = null) + { + $this->add($pull_request_id, $pull_request_html_url); + $this->deploy($pull_request_id, $pull_request_html_url); + } + public function deploy(int $pull_request_id, ?string $pull_request_html_url = null) { try { @@ -180,7 +186,7 @@ public function stop(int $pull_request_id) instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false); } } - GetContainersStatus::dispatchSync($this->application->destination->server); + GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high'); $this->dispatch('reloadWindow'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Application/Rollback.php b/app/Livewire/Project/Application/Rollback.php index 41fe598b1..ed0ac1cef 100644 --- a/app/Livewire/Project/Application/Rollback.php +++ b/app/Livewire/Project/Application/Rollback.php @@ -3,7 +3,6 @@ namespace App\Livewire\Project\Application; use App\Models\Application; -use Illuminate\Support\Str; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -50,16 +49,16 @@ public function loadImages($showToast = false) $output = instant_remote_process([ "docker inspect --format='{{.Config.Image}}' {$this->application->uuid}", ], $this->application->destination->server, throwError: false); - $current_tag = Str::of($output)->trim()->explode(':'); + $current_tag = str($output)->trim()->explode(':'); $this->current = data_get($current_tag, 1); $output = instant_remote_process([ "docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'", ], $this->application->destination->server); - $this->images = Str::of($output)->trim()->explode("\n")->filter(function ($item) use ($image) { - return Str::of($item)->contains($image); + $this->images = str($output)->trim()->explode("\n")->filter(function ($item) use ($image) { + return str($item)->contains($image); })->map(function ($item) { - $item = Str::of($item)->explode('#'); + $item = str($item)->explode('#'); if ($item[1] === $this->current) { // $is_current = true; } diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 875a36141..ffdbe95c3 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -46,10 +46,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -87,13 +85,12 @@ public function instantSave() return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index d6c4eb2ce..f81f4a2f0 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -44,10 +44,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -102,13 +100,12 @@ public function instantSave() return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 61dafa76f..6435f6781 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -2,17 +2,10 @@ namespace App\Livewire\Project\Database; -use App\Actions\Database\StartClickhouse; -use App\Actions\Database\StartDragonfly; -use App\Actions\Database\StartKeydb; -use App\Actions\Database\StartMariadb; -use App\Actions\Database\StartMongodb; -use App\Actions\Database\StartMysql; -use App\Actions\Database\StartPostgresql; -use App\Actions\Database\StartRedis; +use App\Actions\Database\RestartDatabase; +use App\Actions\Database\StartDatabase; use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; -use App\Jobs\ContainerStatusJob; use Livewire\Component; class Heading extends Component @@ -48,7 +41,6 @@ public function activityFinished() public function check_status($showNotification = false) { GetContainersStatus::run($this->database->destination->server); - // dispatch_sync(new ContainerStatusJob($this->database->destination->server)); $this->database->refresh(); if ($showNotification) { $this->dispatch('success', 'Database status updated.'); @@ -68,32 +60,15 @@ public function stop() $this->check_status(); } + public function restart() + { + $activity = RestartDatabase::run($this->database); + $this->dispatch('activityMonitor', $activity->id); + } + public function start() { - if ($this->database->type() === 'standalone-postgresql') { - $activity = StartPostgresql::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-redis') { - $activity = StartRedis::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mongodb') { - $activity = StartMongodb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mysql') { - $activity = StartMysql::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-mariadb') { - $activity = StartMariadb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-keydb') { - $activity = StartKeydb::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-dragonfly') { - $activity = StartDragonfly::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } elseif ($this->database->type() === 'standalone-clickhouse') { - $activity = StartClickhouse::run($this->database); - $this->dispatch('activityMonitor', $activity->id); - } + $activity = StartDatabase::run($this->database); + $this->dispatch('activityMonitor', $activity->id); } } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index 381711946..2b78c9f10 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -46,10 +46,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -108,13 +106,12 @@ public function instantSave() return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 8b4b35d11..858d7b383 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -52,10 +52,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -114,13 +112,12 @@ public function instantSave() return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index ee639ae41..5a5ef8a62 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -50,10 +50,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -115,13 +113,12 @@ public function instantSave() return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Mysql/General.php b/app/Livewire/Project/Database/Mysql/General.php index fc0767109..58d8e03a8 100644 --- a/app/Livewire/Project/Database/Mysql/General.php +++ b/app/Livewire/Project/Database/Mysql/General.php @@ -52,10 +52,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -113,13 +111,12 @@ public function instantSave() return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Postgresql/General.php b/app/Livewire/Project/Database/Postgresql/General.php index 38cac2e5c..eabbbd679 100644 --- a/app/Livewire/Project/Database/Postgresql/General.php +++ b/app/Livewire/Project/Database/Postgresql/General.php @@ -25,7 +25,14 @@ class General extends Component public ?string $db_url_public = null; - protected $listeners = ['refresh', 'save_init_script', 'delete_init_script']; + public function getListeners() + { + return [ + 'refresh', + 'save_init_script', + 'delete_init_script', + ]; + } protected $rules = [ 'database.name' => 'required', @@ -62,10 +69,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -103,13 +108,12 @@ public function instantSave() return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index b5c1dd881..a7ce0161a 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -46,10 +46,8 @@ class General extends Component public function mount() { - $this->db_url = $this->database->get_db_url(true); - if ($this->database->is_public) { - $this->db_url_public = $this->database->get_db_url(); - } + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); } @@ -102,13 +100,12 @@ public function instantSave() return; } StartDatabaseProxy::run($this->database); - $this->db_url_public = $this->database->get_db_url(); $this->dispatch('success', 'Database is now publicly accessible.'); } else { StopDatabaseProxy::run($this->database); - $this->db_url_public = null; $this->dispatch('success', 'Database is no longer publicly accessible.'); } + $this->db_url_public = $this->database->external_db_url; $this->database->save(); } catch (\Throwable $e) { $this->database->is_public = ! $this->database->is_public; diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index 65a98b37f..fdad052c7 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -6,7 +6,6 @@ use App\Models\Project; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Str; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -29,9 +28,9 @@ public function submit() $this->validate([ 'dockerImage' => 'required', ]); - $image = Str::of($this->dockerImage)->before(':'); - if (Str::of($this->dockerImage)->contains(':')) { - $tag = Str::of($this->dockerImage)->after(':'); + $image = str($this->dockerImage)->before(':'); + if (str($this->dockerImage)->contains(':')) { + $tag = str($this->dockerImage)->after(':'); } else { $tag = 'latest'; } diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 690149cc4..a35a92516 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -193,7 +193,7 @@ private function get_git_source() return; } - if (Str::of($this->repository_url)->startsWith('http')) { + if (str($this->repository_url)->startsWith('http')) { $this->git_host = $this->repository_url_parsed->getHost(); $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); $this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git'); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 739061f1f..7ac7883dc 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -128,8 +128,8 @@ public function load_branch() ) { $this->repository_url = $this->repository_url.'.git'; } - if (str($this->repository_url)->contains('github.com')) { - $this->repository_url = str($this->repository_url)->before('.git')->value(); + if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) { + $this->repository_url = str($this->repository_url)->beforeLast('.git')->value(); } } catch (\Throwable $e) { return handleError($e, $this); @@ -140,7 +140,6 @@ public function load_branch() $this->get_branch(); $this->selected_branch = $this->git_branch; } catch (\Throwable $e) { - ray($e->getMessage()); if (! $this->branch_found && $this->git_branch == 'main') { try { $this->git_branch = 'master'; diff --git a/app/Livewire/Project/New/Select.php b/app/Livewire/Project/New/Select.php index b8d186dab..b25290f71 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -176,10 +176,12 @@ public function setType(string $type) return; } - // if (count($this->servers) === 1) { - // $server = $this->servers->first(); - // $this->setServer($server); - // } + if (count($this->servers) === 1) { + $server = $this->servers->first(); + if ($server instanceof Server) { + $this->setServer($server); + } + } if (! is_null($this->server)) { $foundServer = $this->servers->where('id', $this->server->id)->first(); if ($foundServer) { @@ -195,6 +197,13 @@ public function setServer(Server $server) $this->server = $server; $this->standaloneDockers = $server->standaloneDockers; $this->swarmDockers = $server->swarmDockers; + $count = count($this->standaloneDockers) + count($this->swarmDockers); + if ($count === 1) { + $docker = $this->standaloneDockers->first() ?? $this->swarmDockers->first(); + if ($docker) { + $this->setDestination($docker->uuid); + } + } $this->current_step = 'destinations'; } diff --git a/app/Livewire/Project/Resource/EnvironmentSelect.php b/app/Livewire/Project/Resource/EnvironmentSelect.php new file mode 100644 index 000000000..efb1b6ca2 --- /dev/null +++ b/app/Livewire/Project/Resource/EnvironmentSelect.php @@ -0,0 +1,35 @@ +selectedEnvironment = request()->route('environment_name'); + $this->project_uuid = request()->route('project_uuid'); + } + + public function updatedSelectedEnvironment($value) + { + if ($value === 'edit') { + return redirect()->route('project.show', [ + 'project_uuid' => $this->project_uuid, + ]); + } else { + return redirect()->route('project.resource.index', [ + 'project_uuid' => $this->project_uuid, + 'environment_name' => $value, + ]); + } + } +} diff --git a/app/Livewire/Project/Service/EditCompose.php b/app/Livewire/Project/Service/EditCompose.php index fd4d684b1..f67b95a8a 100644 --- a/app/Livewire/Project/Service/EditCompose.php +++ b/app/Livewire/Project/Service/EditCompose.php @@ -11,12 +11,25 @@ class EditCompose extends Component public $serviceId; + protected $listeners = ['refreshEnvs', 'envsUpdated']; + protected $rules = [ 'service.docker_compose_raw' => 'required', 'service.docker_compose' => 'required', 'service.is_container_label_escape_enabled' => 'required', ]; + public function envsUpdated() + { + $this->dispatch('saveCompose', $this->service->docker_compose_raw); + $this->refreshEnvs(); + } + + public function refreshEnvs() + { + $this->service = Service::find($this->serviceId); + } + public function mount() { $this->service = Service::find($this->serviceId); diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 201ebf58f..2eea0891f 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -14,7 +14,6 @@ use App\Models\StandaloneMysql; use App\Models\StandalonePostgresql; use App\Models\StandaloneRedis; -use Illuminate\Support\Str; use Livewire\Component; class FileStorage extends Component @@ -37,9 +36,9 @@ class FileStorage extends Component public function mount() { $this->resource = $this->fileStorage->service; - if (Str::of($this->fileStorage->fs_path)->startsWith('.')) { + if (str($this->fileStorage->fs_path)->startsWith('.')) { $this->workdir = $this->resource->service?->workdir(); - $this->fs_path = Str::of($this->fileStorage->fs_path)->after('.'); + $this->fs_path = str($this->fileStorage->fs_path)->after('.'); } else { $this->workdir = null; $this->fs_path = $this->fileStorage->fs_path; diff --git a/app/Livewire/Project/Service/StackForm.php b/app/Livewire/Project/Service/StackForm.php index 05917f895..3f62202c8 100644 --- a/app/Livewire/Project/Service/StackForm.php +++ b/app/Livewire/Project/Service/StackForm.php @@ -75,14 +75,12 @@ public function submit() $this->service->parse(); $this->service->refresh(); $this->service->saveComposeConfigs(); - $this->dispatch('refreshStacks'); $this->dispatch('refreshEnvs'); $this->dispatch('success', 'Service saved.'); } catch (\Throwable $e) { return handleError($e, $this); } finally { if (is_null($this->service->config_hash)) { - ray('asdf'); $this->service->isConfigurationChanged(true); } else { $this->dispatch('configurationChanged'); diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 4c06bfe23..d67dae19e 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -112,7 +112,6 @@ public function saveVariables($isPreview) $this->resource->environment_variables_preview()->whereNotIn('key', array_keys($variables))->delete(); } else { $variables = parseEnvFormatToArray($this->variables); - ray($variables, $this->variables); $this->resource->environment_variables()->whereNotIn('key', array_keys($variables))->delete(); } foreach ($variables as $key => $variable) { diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index e77c05d6b..c21d899e5 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -112,7 +112,7 @@ public function submit() $this->serialize(); $this->env->save(); $this->dispatch('success', 'Environment variable updated.'); - $this->dispatch('refreshEnvs'); + $this->dispatch('envsUpdated'); } catch (\Exception $e) { return handleError($e); } diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index dc3a62c56..343915d9c 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Shared; +use App\Actions\Server\RunCommand; use App\Models\Application; use App\Models\Server; use App\Models\Service; @@ -137,7 +138,7 @@ public function runCommand() } else { $exec = "docker exec {$container_name} {$cmd}"; } - $activity = remote_process([$exec], $server, ignore_errors: true); + $activity = RunCommand::run(server: $server, command: $exec); $this->dispatch('activityMonitor', $activity->id); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index 008d743ed..5af0a6a50 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -59,15 +59,6 @@ public function loadContainers($server_id) } } - public function loadMetrics() - { - return; - $server = data_get($this->resource, 'destination.server'); - if ($server->isFunctional()) { - $this->cpu = $server->getMetrics(); - } - } - public function mount() { try { @@ -122,7 +113,6 @@ public function mount() } - $this->loadMetrics(); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php new file mode 100644 index 000000000..d9d7dd3ef --- /dev/null +++ b/app/Livewire/Project/Shared/Metrics.php @@ -0,0 +1,64 @@ +poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $metrics = $this->resource->getMetrics($this->interval); + $cpuMetrics = collect($metrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $memoryMetrics = collect($metrics)->map(function ($metric) { + return [$metric[0], $metric[2]]; + }); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ + 'seriesData' => $cpuMetrics, + ]); + $this->dispatch("refreshChartData-{$this->chartId}-memory", [ + 'seriesData' => $memoryMetrics, + ]); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } + + public function render() + { + return view('livewire.project.shared.metrics'); + } +} diff --git a/app/Livewire/Project/Show.php b/app/Livewire/Project/Show.php index d5d660017..1082f078c 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -9,6 +9,8 @@ class Show extends Component { public Project $project; + public $environments; + public function mount() { $projectUuid = request()->route('project_uuid'); @@ -18,7 +20,8 @@ public function mount() if (! $project) { return redirect()->route('dashboard'); } - $project->load(['environments']); + + $this->environments = $project->environments->sortBy('created_at'); $this->project = $project; } diff --git a/app/Livewire/RunCommand.php b/app/Livewire/RunCommand.php index fc7f1eefc..c2d3adeea 100644 --- a/app/Livewire/RunCommand.php +++ b/app/Livewire/RunCommand.php @@ -2,6 +2,7 @@ namespace App\Livewire; +use App\Actions\Server\RunCommand as ServerRunCommand; use App\Models\Server; use Livewire\Component; @@ -33,7 +34,7 @@ public function runCommand() { $this->validate(); try { - $activity = remote_process([$this->command], Server::where('uuid', $this->server)->first(), ignore_errors: true); + $activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command); $this->dispatch('activityMonitor', $activity->id); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Security/ApiTokens.php b/app/Livewire/Security/ApiTokens.php index c485a6a3a..ff8679d21 100644 --- a/app/Livewire/Security/ApiTokens.php +++ b/app/Livewire/Security/ApiTokens.php @@ -10,6 +10,12 @@ class ApiTokens extends Component public $tokens = []; + public bool $viewSensitiveData = false; + + public bool $readOnly = true; + + public array $permissions = ['read-only']; + public function render() { return view('livewire.security.api-tokens'); @@ -17,7 +23,33 @@ public function render() public function mount() { - $this->tokens = auth()->user()->tokens; + $this->tokens = auth()->user()->tokens->sortByDesc('created_at'); + } + + public function updatedViewSensitiveData() + { + if ($this->viewSensitiveData) { + $this->permissions[] = 'view:sensitive'; + $this->permissions = array_diff($this->permissions, ['*']); + } else { + $this->permissions = array_diff($this->permissions, ['view:sensitive']); + } + if (count($this->permissions) == 0) { + $this->permissions = ['*']; + } + } + + public function updatedReadOnly() + { + if ($this->readOnly) { + $this->permissions[] = 'read-only'; + $this->permissions = array_diff($this->permissions, ['*']); + } else { + $this->permissions = array_diff($this->permissions, ['read-only']); + } + if (count($this->permissions) == 0) { + $this->permissions = ['*']; + } } public function addNewToken() @@ -26,7 +58,13 @@ public function addNewToken() $this->validate([ 'description' => 'required|min:3|max:255', ]); - $token = auth()->user()->createToken($this->description); + // if ($this->viewSensitiveData) { + // $this->permissions[] = 'view:sensitive'; + // } + // if ($this->readOnly) { + // $this->permissions[] = 'read-only'; + // } + $token = auth()->user()->createToken($this->description, $this->permissions); $this->tokens = auth()->user()->tokens; session()->flash('token', $token->plainTextToken); } catch (\Exception $e) { diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php new file mode 100644 index 000000000..0921c7fa4 --- /dev/null +++ b/app/Livewire/Server/Charts.php @@ -0,0 +1,62 @@ +poll || $this->interval <= 10) { + $this->loadData(); + if ($this->interval > 10) { + $this->poll = false; + } + } + } + + public function loadData() + { + try { + $cpuMetrics = $this->server->getCpuMetrics($this->interval); + $memoryMetrics = $this->server->getMemoryMetrics($this->interval); + $cpuMetrics = collect($cpuMetrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $memoryMetrics = collect($memoryMetrics)->map(function ($metric) { + return [$metric[0], $metric[1]]; + }); + $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ + 'seriesData' => $cpuMetrics, + ]); + $this->dispatch("refreshChartData-{$this->chartId}-memory", [ + 'seriesData' => $memoryMetrics, + ]); + + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function setInterval() + { + if ($this->interval <= 10) { + $this->poll = true; + } + $this->loadData(); + } +} diff --git a/app/Livewire/Server/ConfigureCloudflareTunnels.php b/app/Livewire/Server/ConfigureCloudflareTunnels.php index 7d2103e37..f7306a5b5 100644 --- a/app/Livewire/Server/ConfigureCloudflareTunnels.php +++ b/app/Livewire/Server/ConfigureCloudflareTunnels.php @@ -21,7 +21,7 @@ public function alreadyConfigured() $server->settings->is_cloudflare_tunnel = true; $server->settings->save(); $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } @@ -37,7 +37,7 @@ public function submit() $server->save(); $server->settings->save(); $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php index 263ff6367..5616123a5 100644 --- a/app/Livewire/Server/Form.php +++ b/app/Livewire/Server/Form.php @@ -2,6 +2,9 @@ namespace App\Livewire\Server; +use App\Actions\Server\StartSentinel; +use App\Actions\Server\StopSentinel; +use App\Jobs\PullSentinelImageJob; use App\Models\Server; use Livewire\Component; @@ -36,7 +39,12 @@ class Form extends Component 'server.settings.is_build_server' => 'required|boolean', 'server.settings.concurrent_builds' => 'required|integer|min:1', 'server.settings.dynamic_timeout' => 'required|integer|min:1', + 'server.settings.is_metrics_enabled' => 'required|boolean', + 'server.settings.metrics_token' => 'required', + 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', + 'server.settings.metrics_history_days' => 'required|integer|min:1', 'wildcard_domain' => 'nullable|url', + 'server.settings.is_server_api_enabled' => 'required|boolean', ]; protected $validationAttributes = [ @@ -52,7 +60,11 @@ class Form extends Component 'server.settings.is_build_server' => 'Build Server', 'server.settings.concurrent_builds' => 'Concurrent Builds', 'server.settings.dynamic_timeout' => 'Dynamic Timeout', - + 'server.settings.is_metrics_enabled' => 'Metrics', + 'server.settings.metrics_token' => 'Metrics Token', + 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', + 'server.settings.metrics_history_days' => 'Metrics History', + 'server.settings.is_server_api_enabled' => 'Server API', ]; public function mount() @@ -69,18 +81,59 @@ public function serverInstalled() public function updatedServerSettingsIsBuildServer() { - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); $this->dispatch('serverRefresh'); $this->dispatch('proxyStatusUpdated'); } + public function checkPortForServerApi() + { + try { + if ($this->server->settings->is_server_api_enabled === true) { + $this->server->checkServerApi(); + $this->dispatch('success', 'Server API is reachable.'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function instantSave() { try { refresh_server_connection($this->server->privateKey); $this->validateServer(false); $this->server->settings->save(); + $this->server->save(); $this->dispatch('success', 'Server updated.'); + $this->dispatch('refreshServerShow'); + if ($this->server->isSentinelEnabled()) { + PullSentinelImageJob::dispatchSync($this->server); + ray('Sentinel is enabled'); + if ($this->server->settings->isDirty('is_metrics_enabled')) { + $this->dispatch('reloadWindow'); + } + if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) { + ray('Starting sentinel'); + + } + } else { + ray('Sentinel is not enabled'); + StopSentinel::dispatch($this->server); + } + // $this->checkPortForServerApi(); + + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function restartSentinel() + { + try { + $version = get_latest_sentinel_version(); + StartSentinel::run($this->server, $version, true); + $this->dispatch('success', 'Sentinel restarted.'); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 8d1ece1c6..a170ee029 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -6,7 +6,6 @@ use App\Actions\Proxy\SaveConfiguration; use App\Actions\Proxy\StartProxy; use App\Models\Server; -use Illuminate\Support\Str; use Livewire\Component; class Proxy extends Component @@ -79,7 +78,7 @@ public function loadProxyConfiguration() { try { $this->proxy_settings = CheckConfiguration::run($this->server); - if (Str::of($this->proxy_settings)->contains('--api.dashboard=true') && Str::of($this->proxy_settings)->contains('--api.insecure=true')) { + if (str($this->proxy_settings)->contains('--api.dashboard=true') && str($this->proxy_settings)->contains('--api.insecure=true')) { $this->dispatch('traefikDashboardAvailable', true); } else { $this->dispatch('traefikDashboardAvailable', false); diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index 7ebf90115..0751b186e 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -14,7 +14,7 @@ class Show extends Component public $parameters = []; - protected $listeners = ['serverInstalled' => '$refresh']; + protected $listeners = ['refreshServerShow' => '$refresh']; public function mount() { diff --git a/app/Livewire/Server/ValidateAndInstall.php b/app/Livewire/Server/ValidateAndInstall.php index bd33937e0..422cae779 100644 --- a/app/Livewire/Server/ValidateAndInstall.php +++ b/app/Livewire/Server/ValidateAndInstall.php @@ -143,7 +143,8 @@ public function validateDockerVersion() } else { $this->docker_version = $this->server->validateDockerEngineVersion(); if ($this->docker_version) { - $this->dispatch('serverInstalled'); + $this->dispatch('refreshServerShow'); + $this->dispatch('refreshBoardingIndex'); $this->dispatch('success', 'Server validated.'); } else { $this->error = 'Docker Engine version is not 22+. Please install Docker manually before continuing: documentation.'; diff --git a/app/Livewire/Settings/Configuration.php b/app/Livewire/Settings/Configuration.php index 4dfa16e30..7439e112f 100644 --- a/app/Livewire/Settings/Configuration.php +++ b/app/Livewire/Settings/Configuration.php @@ -18,7 +18,8 @@ class Configuration extends Component public bool $is_dns_validation_enabled; - // public bool $next_channel; + public bool $is_api_enabled; + protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; protected Server $server; @@ -29,6 +30,8 @@ class Configuration extends Component 'settings.public_port_min' => 'required', 'settings.public_port_max' => 'required', 'settings.custom_dns_servers' => 'nullable', + 'settings.instance_name' => 'nullable', + 'settings.allowed_ips' => 'nullable', ]; protected $validationAttributes = [ @@ -37,6 +40,7 @@ class Configuration extends Component 'settings.public_port_min' => 'Public port min', 'settings.public_port_max' => 'Public port max', 'settings.custom_dns_servers' => 'Custom DNS servers', + 'settings.allowed_ips' => 'Allowed IPs', ]; public function mount() @@ -44,8 +48,8 @@ public function mount() $this->do_not_track = $this->settings->do_not_track; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled; - // $this->next_channel = $this->settings->next_channel; $this->is_dns_validation_enabled = $this->settings->is_dns_validation_enabled; + $this->is_api_enabled = $this->settings->is_api_enabled; } public function instantSave() @@ -54,12 +58,7 @@ public function instantSave() $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; $this->settings->is_registration_enabled = $this->is_registration_enabled; $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - // if ($this->next_channel) { - // $this->settings->next_channel = false; - // $this->next_channel = false; - // } else { - // $this->settings->next_channel = $this->next_channel; - // } + $this->settings->is_api_enabled = $this->is_api_enabled; $this->settings->save(); $this->dispatch('success', 'Settings updated!'); } @@ -93,6 +92,13 @@ public function submit() $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->unique(); $this->settings->custom_dns_servers = $this->settings->custom_dns_servers->implode(','); + $this->settings->allowed_ips = str($this->settings->allowed_ips)->replaceEnd(',', '')->trim(); + $this->settings->allowed_ips = str($this->settings->allowed_ips)->trim()->explode(',')->map(function ($ip) { + return str($ip)->trim(); + }); + $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); + $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); + $this->settings->save(); $this->server->setupDynamicProxyConfiguration(); if (! $error_show) { diff --git a/app/Livewire/Subscription/Actions.php b/app/Livewire/Subscription/Actions.php index db1f565a6..1388d3244 100644 --- a/app/Livewire/Subscription/Actions.php +++ b/app/Livewire/Subscription/Actions.php @@ -3,7 +3,6 @@ namespace App\Livewire\Subscription; use App\Models\Team; -use Illuminate\Support\Facades\Http; use Livewire\Component; class Actions extends Component @@ -15,70 +14,6 @@ public function mount() $this->server_limits = Team::serverLimit(); } - public function cancel() - { - try { - $subscription_id = currentTeam()->subscription->lemon_subscription_id; - if (! $subscription_id) { - throw new \Exception('No subscription found'); - } - $response = Http::withHeaders([ - 'Accept' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json', - 'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'), - ])->delete('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id); - $json = $response->json(); - if ($response->failed()) { - $error = data_get($json, 'errors.0.status'); - if ($error === '404') { - throw new \Exception('Subscription not found.'); - } - throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.')); - } else { - $this->dispatch('success', 'Subscription cancelled successfully. Reloading in 5s.'); - $this->dispatch('reloadWindow', 5000); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function resume() - { - try { - $subscription_id = currentTeam()->subscription->lemon_subscription_id; - if (! $subscription_id) { - throw new \Exception('No subscription found'); - } - $response = Http::withHeaders([ - 'Accept' => 'application/vnd.api+json', - 'Content-Type' => 'application/vnd.api+json', - 'Authorization' => 'Bearer '.config('subscription.lemon_squeezy_api_key'), - ])->patch('https://api.lemonsqueezy.com/v1/subscriptions/'.$subscription_id, [ - 'data' => [ - 'type' => 'subscriptions', - 'id' => $subscription_id, - 'attributes' => [ - 'cancelled' => false, - ], - ], - ]); - $json = $response->json(); - if ($response->failed()) { - $error = data_get($json, 'errors.0.status'); - if ($error === '404') { - throw new \Exception('Subscription not found.'); - } - throw new \Exception(data_get($json, 'errors.0.title', 'Something went wrong. Please try again later.')); - } else { - $this->dispatch('success', 'Subscription resumed successfully. Reloading in 5s.'); - $this->dispatch('reloadWindow', 5000); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - public function stripeCustomerPortal() { $session = getStripeCustomerPortalSession(currentTeam()); diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index cc69e6650..0fa9e980c 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -52,7 +52,7 @@ private function generate_invite_link(bool $sendEmail = false) if (is_null($user)) { $password = Str::password(); $user = User::create([ - 'name' => Str::of($this->email)->before('@'), + 'name' => str($this->email)->before('@'), 'email' => $this->email, 'password' => Hash::make($password), 'force_password_reset' => true, diff --git a/app/Models/Application.php b/app/Models/Application.php index 6e55f6626..47487d1f8 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -8,12 +8,95 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; use Illuminate\Support\Str; +use OpenApi\Attributes as OA; use RuntimeException; use Spatie\Activitylog\Models\Activity; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; use Visus\Cuid2\Cuid2; +#[OA\Schema( + description: 'Application model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer', 'description' => 'The application identifier in the database.'], + 'description' => ['type' => 'string', 'nullable' => true, 'description' => 'The application description.'], + 'repository_project_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'The repository project identifier.'], + 'uuid' => ['type' => 'string', 'description' => 'The application UUID.'], + 'name' => ['type' => 'string', 'description' => 'The application name.'], + 'fqdn' => ['type' => 'string', 'nullable' => true, 'description' => 'The application domains.'], + 'config_hash' => ['type' => 'string', 'description' => 'Configuration hash.'], + 'git_repository' => ['type' => 'string', 'description' => 'Git repository URL.'], + 'git_branch' => ['type' => 'string', 'description' => 'Git branch.'], + 'git_commit_sha' => ['type' => 'string', 'description' => 'Git commit SHA.'], + 'git_full_url' => ['type' => 'string', 'nullable' => true, 'description' => 'Git full URL.'], + 'docker_registry_image_name' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image name.'], + 'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker registry image tag.'], + 'build_pack' => ['type' => 'string', 'description' => 'Build pack.', 'enum' => ['nixpacks', 'static', 'dockerfile', 'dockercompose']], + 'static_image' => ['type' => 'string', 'description' => 'Static image used when static site is deployed.'], + 'install_command' => ['type' => 'string', 'description' => 'Install command.'], + 'build_command' => ['type' => 'string', 'description' => 'Build command.'], + 'start_command' => ['type' => 'string', 'description' => 'Start command.'], + 'ports_exposes' => ['type' => 'string', 'description' => 'Ports exposes.'], + 'ports_mappings' => ['type' => 'string', 'nullable' => true, 'description' => 'Ports mappings.'], + 'base_directory' => ['type' => 'string', 'description' => 'Base directory for all commands.'], + 'publish_directory' => ['type' => 'string', 'description' => 'Publish directory.'], + 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Health check enabled.'], + 'health_check_path' => ['type' => 'string', 'description' => 'Health check path.'], + 'health_check_port' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check port.'], + 'health_check_host' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check host.'], + 'health_check_method' => ['type' => 'string', 'description' => 'Health check method.'], + 'health_check_return_code' => ['type' => 'integer', 'description' => 'Health check return code.'], + 'health_check_scheme' => ['type' => 'string', 'description' => 'Health check scheme.'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check response text.'], + 'health_check_interval' => ['type' => 'integer', 'description' => 'Health check interval in seconds.'], + 'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'], + 'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'], + 'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation.'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit.'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true, 'description' => 'CPU set.'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares.'], + 'status' => ['type' => 'string', 'description' => 'Application status.'], + 'preview_url_template' => ['type' => 'string', 'description' => 'Preview URL template.'], + 'destination_type' => ['type' => 'string', 'description' => 'Destination type.'], + 'destination_id' => ['type' => 'integer', 'description' => 'Destination identifier.'], + 'source_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Source identifier.'], + 'private_key_id' => ['type' => 'integer', 'nullable' => true, 'description' => 'Private key identifier.'], + 'environment_id' => ['type' => 'integer', 'description' => 'Environment identifier.'], + 'dockerfile' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile content. Used for dockerfile build pack.'], + 'dockerfile_location' => ['type' => 'string', 'description' => 'Dockerfile location.'], + 'custom_labels' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom labels.'], + 'dockerfile_target_build' => ['type' => 'string', 'nullable' => true, 'description' => 'Dockerfile target build.'], + 'manual_webhook_secret_github' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitHub.'], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for GitLab.'], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Bitbucket.'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'nullable' => true, 'description' => 'Manual webhook secret for Gitea.'], + 'docker_compose_location' => ['type' => 'string', 'description' => 'Docker compose location.'], + 'docker_compose' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose content. Used for docker compose build pack.'], + 'docker_compose_raw' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose raw content.'], + 'docker_compose_domains' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose domains.'], + 'docker_compose_custom_start_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom start command.'], + 'docker_compose_custom_build_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Docker compose custom build command.'], + 'swarm_replicas' => ['type' => 'integer', 'nullable' => true, 'description' => 'Swarm replicas. Only used for swarm deployments.'], + 'swarm_placement_constraints' => ['type' => 'string', 'nullable' => true, 'description' => 'Swarm placement constraints. Only used for swarm deployments.'], + 'custom_docker_run_options' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom docker run options.'], + 'post_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command.'], + 'post_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Post deployment command container.'], + 'pre_deployment_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command.'], + 'pre_deployment_command_container' => ['type' => 'string', 'nullable' => true, 'description' => 'Pre deployment command container.'], + 'watch_paths' => ['type' => 'string', 'nullable' => true, 'description' => 'Watch paths.'], + 'custom_healthcheck_found' => ['type' => 'boolean', 'description' => 'Custom healthcheck found.'], + 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], + 'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was created.'], + 'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was last updated.'], + 'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'], + ] +)] + class Application extends BaseModel { use SoftDeletes; @@ -28,11 +111,11 @@ protected static function booted() } $application->forceFill([ 'fqdn' => $application->fqdn, - 'install_command' => Str::of($application->install_command)->trim(), - 'build_command' => Str::of($application->build_command)->trim(), - 'start_command' => Str::of($application->start_command)->trim(), - 'base_directory' => Str::of($application->base_directory)->trim(), - 'publish_directory' => Str::of($application->publish_directory)->trim(), + 'install_command' => str($application->install_command)->trim(), + 'build_command' => str($application->build_command)->trim(), + 'start_command' => str($application->start_command)->trim(), + 'base_directory' => str($application->base_directory)->trim(), + 'publish_directory' => str($application->publish_directory)->trim(), ]); }); static::created(function ($application) { @@ -60,6 +143,11 @@ protected static function booted() }); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); + } + public function delete_configurations() { $server = data_get($this, 'destination.server'); @@ -228,18 +316,13 @@ public function gitCommits(): Attribute public function gitCommitLink($link): string { - if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { + if (! is_null(data_get($this, 'source.html_url')) && ! is_null(data_get($this, 'git_repository')) && ! is_null(data_get($this, 'git_branch'))) { if (str($this->source->html_url)->contains('bitbucket')) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}"; } return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}"; } - if (strpos($this->git_repository, 'git@') === 0) { - $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); - - return "https://{$git_repository}/commit/{$link}"; - } if (str($this->git_repository)->contains('bitbucket')) { $git_repository = str_replace('.git', '', $this->git_repository); $url = Url::fromString($git_repository); @@ -248,6 +331,14 @@ public function gitCommitLink($link): string return $url->__toString(); } + if (strpos($this->git_repository, 'git@') === 0) { + $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); + if (data_get($this, 'source.html_url')) { + return "{$this->source->html_url}/{$git_repository}/commit/{$link}"; + } + + return "{$git_repository}/commit/{$link}"; + } return $this->git_repository; } @@ -532,7 +623,7 @@ public function isDeploymentInprogress() public function get_last_successful_deployment() { - return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); + return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', ApplicationDeploymentStatus::FINISHED)->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first(); } public function get_last_days_deployments() @@ -899,9 +990,9 @@ public function parseRawCompose() $type = null; $source = null; if (is_string($volume)) { - $source = Str::of($volume)->before(':'); + $source = str($volume)->before(':'); if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) { - $type = Str::of('bind'); + $type = str('bind'); } } elseif (is_array($volume)) { $type = data_get_str($volume, 'type'); @@ -961,11 +1052,7 @@ public function loadComposeFile($isInit = false) ['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: '.'); $workdir = rtrim($this->base_directory, '/'); $composeFile = $this->docker_compose_location; - // $prComposeFile = $this->docker_compose_pr_location; $fileList = collect([".$workdir$composeFile"]); - // if ($composeFile !== $prComposeFile) { - // $fileList->push(".$prComposeFile"); - // } $commands = collect([ "rm -rf /tmp/{$uuid}", "mkdir -p /tmp/{$uuid}", @@ -1014,7 +1101,6 @@ public function loadComposeFile($isInit = false) return [ 'parsedServices' => $parsedServices, 'initialDockerComposeLocation' => $this->docker_compose_location, - 'initialDockerComposePrLocation' => $this->docker_compose_pr_location, ]; } @@ -1167,4 +1253,44 @@ public function generate_preview_fqdn(int $pull_request_id) return $preview; } + + public static function getDomainsByUuid(string $uuid): array + { + $application = self::where('uuid', $uuid)->first(); + + if ($application) { + return $application->fqdns; + } + + return []; + } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/ApplicationDeploymentQueue.php b/app/Models/ApplicationDeploymentQueue.php index b1c595046..90d7608cc 100644 --- a/app/Models/ApplicationDeploymentQueue.php +++ b/app/Models/ApplicationDeploymentQueue.php @@ -4,7 +4,37 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Carbon; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'Project model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'application_id' => ['type' => 'string'], + 'deployment_uuid' => ['type' => 'string'], + 'pull_request_id' => ['type' => 'integer'], + 'force_rebuild' => ['type' => 'boolean'], + 'commit' => ['type' => 'string'], + 'status' => ['type' => 'string'], + 'is_webhook' => ['type' => 'boolean'], + 'is_api' => ['type' => 'boolean'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + 'logs' => ['type' => 'string'], + 'current_process_id' => ['type' => 'string'], + 'restart_only' => ['type' => 'boolean'], + 'git_type' => ['type' => 'string'], + 'server_id' => ['type' => 'integer'], + 'application_name' => ['type' => 'string'], + 'server_name' => ['type' => 'string'], + 'deployment_url' => ['type' => 'string'], + 'destination_id' => ['type' => 'string'], + 'only_this_server' => ['type' => 'boolean'], + 'rollback' => ['type' => 'boolean'], + 'commit_message' => ['type' => 'string'], + ], +)] class ApplicationDeploymentQueue extends Model { protected $guarded = []; diff --git a/app/Models/Environment.php b/app/Models/Environment.php index e84b6989b..c892d7ba1 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -4,7 +4,20 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'Environment model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string'], + 'project_id' => ['type' => 'integer'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + 'description' => ['type' => 'string'], + ] +)] class Environment extends Model { protected $guarded = []; @@ -27,6 +40,9 @@ public function isEmpty() $this->redis()->count() == 0 && $this->postgresqls()->count() == 0 && $this->mysqls()->count() == 0 && + $this->keydbs()->count() == 0 && + $this->dragonflies()->count() == 0 && + $this->clickhouses()->count() == 0 && $this->mariadbs()->count() == 0 && $this->mongodbs()->count() == 0 && $this->services()->count() == 0; @@ -109,7 +125,7 @@ public function services() protected function name(): Attribute { return Attribute::make( - set: fn (string $value) => strtolower($value), + set: fn (string $value) => str($value)->lower()->trim()->replace('/', '-')->toString(), ); } } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index ff63bca5a..1d2a9dc66 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -6,8 +6,33 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; +use OpenApi\Attributes as OA; use Symfony\Component\Yaml\Yaml; +use Visus\Cuid2\Cuid2; +#[OA\Schema( + description: 'Environment Variable model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'application_id' => ['type' => 'integer'], + 'service_id' => ['type' => 'integer'], + 'database_id' => ['type' => 'integer'], + 'is_build_time' => ['type' => 'boolean'], + 'is_literal' => ['type' => 'boolean'], + 'is_multiline' => ['type' => 'boolean'], + 'is_preview' => ['type' => 'boolean'], + 'is_shared' => ['type' => 'boolean'], + 'is_shown_once' => ['type' => 'boolean'], + 'key' => ['type' => 'string'], + 'value' => ['type' => 'string'], + 'real_value' => ['type' => 'string'], + 'version' => ['type' => 'string'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + ] +)] class EnvironmentVariable extends Model { protected $guarded = []; @@ -25,6 +50,11 @@ class EnvironmentVariable extends Model protected static function booted() { + static::creating(function (Model $model) { + if (! $model->uuid) { + $model->uuid = (string) new Cuid2(); + } + }); static::created(function (EnvironmentVariable $environment_variable) { if ($environment_variable->application_id && ! $environment_variable->is_preview) { $found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first(); @@ -174,7 +204,7 @@ private function get_real_environment_variables(?string $environment_variable = if (str($environment_variable)->startsWith('{{'.$type) && str($environment_variable)->endsWith('}}')) { $variable = Str::after($environment_variable, "{$type}."); $variable = Str::before($variable, '}}'); - $variable = Str::of($variable)->trim()->value; + $variable = str($variable)->trim()->value; if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) { return $variable; } @@ -220,7 +250,7 @@ private function set_environment_variables(?string $environment_variable = null) protected function key(): Attribute { return Attribute::make( - set: fn (string $value) => Str::of($value)->trim(), + set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value, ); } } diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index daf902daf..66ecdd967 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -20,6 +20,17 @@ class GithubApp extends BaseModel 'webhook_secret', ]; + protected static function booted(): void + { + static::deleting(function (GithubApp $github_app) { + $applications_count = Application::where('source_id', $github_app->id)->count(); + if ($applications_count > 0) { + throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); + } + $github_app->privateKey()->delete(); + }); + } + public static function public() { return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get(); @@ -30,15 +41,9 @@ public static function private() return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get(); } - protected static function booted(): void + public function team() { - static::deleting(function (GithubApp $github_app) { - $applications_count = Application::where('source_id', $github_app->id)->count(); - if ($applications_count > 0) { - throw new \Exception('You cannot delete this GitHub App because it is in use by '.$applications_count.' application(s). Delete them first.'); - } - $github_app->privateKey()->delete(); - }); + return $this->belongsTo(Team::class); } public function applications() diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index 452c5ca22..bd3c41a1f 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -17,6 +17,7 @@ class InstanceSettings extends Model implements SendsEmail protected $casts = [ 'resale_license' => 'encrypted', 'smtp_password' => 'encrypted', + 'allowed_ip_ranges' => 'array', ]; public function fqdn(): Attribute @@ -47,4 +48,14 @@ public function getRecepients($notification) return explode(',', $recipients); } + + public function getTitleDisplayName(): string + { + $instanceName = $this->instance_name; + if (! $instanceName) { + return ''; + } + + return "[{$instanceName}]"; + } } diff --git a/app/Models/Kubernetes.php b/app/Models/Kubernetes.php index 2ad7a2110..174cb5bc8 100644 --- a/app/Models/Kubernetes.php +++ b/app/Models/Kubernetes.php @@ -2,6 +2,4 @@ namespace App\Models; -class Kubernetes extends BaseModel -{ -} +class Kubernetes extends BaseModel {} diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index e48b8b405..68e476365 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; class LocalPersistentVolume extends Model { @@ -33,14 +32,14 @@ public function standalone_postgresql() protected function name(): Attribute { return Attribute::make( - set: fn (string $value) => Str::of($value)->trim()->value, + set: fn (string $value) => str($value)->trim()->value, ); } protected function mountPath(): Attribute { return Attribute::make( - set: fn (string $value) => Str::of($value)->trim()->start('/')->value + set: fn (string $value) => str($value)->trim()->start('/')->value ); } @@ -49,7 +48,7 @@ protected function hostPath(): Attribute return Attribute::make( set: function (?string $value) { if ($value) { - return Str::of($value)->trim()->start('/')->value; + return str($value)->trim()->start('/')->value; } else { return $value; } diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 187dfca58..45bc6bc84 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -2,8 +2,24 @@ namespace App\Models; +use OpenApi\Attributes as OA; use phpseclib3\Crypt\PublicKeyLoader; +#[OA\Schema( + description: 'Private Key model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'description' => ['type' => 'string'], + 'private_key' => ['type' => 'string', 'format' => 'private-key'], + 'is_git_related' => ['type' => 'boolean'], + 'team_id' => ['type' => 'integer'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + ], +)] class PrivateKey extends BaseModel { protected $fillable = [ @@ -14,6 +30,17 @@ class PrivateKey extends BaseModel 'team_id', ]; + protected static function booted() + { + static::saving(function ($key) { + $privateKey = data_get($key, 'private_key'); + if (substr($privateKey, -1) !== "\n") { + $key->private_key = $privateKey."\n"; + } + }); + + } + public static function ownedByCurrentTeam(array $select = ['*']) { $selectArray = collect($select)->concat(['id']); diff --git a/app/Models/Project.php b/app/Models/Project.php index acc98e341..d4310e349 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -2,6 +2,23 @@ namespace App\Models; +use OpenApi\Attributes as OA; + +#[OA\Schema( + description: 'Project model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'environments' => new OA\Property( + property: 'environments', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Environment'), + description: 'The environments of the project.' + ), + ] +)] class Project extends BaseModel { protected $guarded = []; @@ -112,4 +129,18 @@ public function databases() { return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); } + + public function default_environment() + { + $default = $this->environments()->where('name', 'production')->first(); + if ($default) { + return $default->name; + } + $default = $this->environments()->get(); + if ($default->count() > 0) { + return $default->sortBy('created_at')->first()->name; + } + + return null; + } } diff --git a/app/Models/Server.php b/app/Models/Server.php index b1419dc0e..fc4fd9892 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -5,20 +5,102 @@ use App\Actions\Server\InstallDocker; use App\Enums\ProxyTypes; use App\Jobs\PullSentinelImageJob; -use App\Notifications\Server\Revived; -use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Illuminate\Support\Stringable; +use OpenApi\Attributes as OA; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; +#[OA\Schema( + description: 'Application model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'repository_project_id' => ['type' => 'integer', 'nullable' => true], + 'uuid' => ['type' => 'string'], + 'name' => ['type' => 'string'], + 'fqdn' => ['type' => 'string'], + 'config_hash' => ['type' => 'string'], + 'git_repository' => ['type' => 'string'], + 'git_branch' => ['type' => 'string'], + 'git_commit_sha' => ['type' => 'string'], + 'git_full_url' => ['type' => 'string', 'nullable' => true], + 'docker_registry_image_name' => ['type' => 'string', 'nullable' => true], + 'docker_registry_image_tag' => ['type' => 'string', 'nullable' => true], + 'build_pack' => ['type' => 'string'], + 'static_image' => ['type' => 'string'], + 'install_command' => ['type' => 'string'], + 'build_command' => ['type' => 'string'], + 'start_command' => ['type' => 'string'], + 'ports_exposes' => ['type' => 'string'], + 'ports_mappings' => ['type' => 'string', 'nullable' => true], + 'base_directory' => ['type' => 'string'], + 'publish_directory' => ['type' => 'string'], + 'health_check_path' => ['type' => 'string'], + 'health_check_port' => ['type' => 'string', 'nullable' => true], + 'health_check_host' => ['type' => 'string'], + 'health_check_method' => ['type' => 'string'], + 'health_check_return_code' => ['type' => 'integer'], + 'health_check_scheme' => ['type' => 'string'], + 'health_check_response_text' => ['type' => 'string', 'nullable' => true], + 'health_check_interval' => ['type' => 'integer'], + 'health_check_timeout' => ['type' => 'integer'], + 'health_check_retries' => ['type' => 'integer'], + 'health_check_start_period' => ['type' => 'integer'], + 'limits_memory' => ['type' => 'string'], + 'limits_memory_swap' => ['type' => 'string'], + 'limits_memory_swappiness' => ['type' => 'integer'], + 'limits_memory_reservation' => ['type' => 'string'], + 'limits_cpus' => ['type' => 'string'], + 'limits_cpuset' => ['type' => 'string', 'nullable' => true], + 'limits_cpu_shares' => ['type' => 'integer'], + 'status' => ['type' => 'string'], + 'preview_url_template' => ['type' => 'string'], + 'destination_type' => ['type' => 'string'], + 'destination_id' => ['type' => 'integer'], + 'source_type' => ['type' => 'string'], + 'source_id' => ['type' => 'integer'], + 'private_key_id' => ['type' => 'integer', 'nullable' => true], + 'environment_id' => ['type' => 'integer'], + 'created_at' => ['type' => 'string', 'format' => 'date-time'], + 'updated_at' => ['type' => 'string', 'format' => 'date-time'], + 'description' => ['type' => 'string', 'nullable' => true], + 'dockerfile' => ['type' => 'string', 'nullable' => true], + 'health_check_enabled' => ['type' => 'boolean'], + 'dockerfile_location' => ['type' => 'string'], + 'custom_labels' => ['type' => 'string'], + 'dockerfile_target_build' => ['type' => 'string', 'nullable' => true], + 'manual_webhook_secret_github' => ['type' => 'string', 'nullable' => true], + 'manual_webhook_secret_gitlab' => ['type' => 'string', 'nullable' => true], + 'docker_compose_location' => ['type' => 'string'], + 'docker_compose' => ['type' => 'string', 'nullable' => true], + 'docker_compose_raw' => ['type' => 'string', 'nullable' => true], + 'docker_compose_domains' => ['type' => 'string', 'nullable' => true], + 'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true], + 'docker_compose_custom_start_command' => ['type' => 'string', 'nullable' => true], + 'docker_compose_custom_build_command' => ['type' => 'string', 'nullable' => true], + 'swarm_replicas' => ['type' => 'integer'], + 'swarm_placement_constraints' => ['type' => 'string', 'nullable' => true], + 'manual_webhook_secret_bitbucket' => ['type' => 'string', 'nullable' => true], + 'custom_docker_run_options' => ['type' => 'string', 'nullable' => true], + 'post_deployment_command' => ['type' => 'string', 'nullable' => true], + 'post_deployment_command_container' => ['type' => 'string', 'nullable' => true], + 'pre_deployment_command' => ['type' => 'string', 'nullable' => true], + 'pre_deployment_command_container' => ['type' => 'string', 'nullable' => true], + 'watch_paths' => ['type' => 'string', 'nullable' => true], + 'custom_healthcheck_found' => ['type' => 'boolean'], + 'manual_webhook_secret_gitea' => ['type' => 'string', 'nullable' => true], + 'redirect' => ['type' => 'string'], + ] +)] + class Server extends BaseModel { use SchemalessAttributesTrait; @@ -30,10 +112,10 @@ protected static function booted() static::saving(function ($server) { $payload = []; if ($server->user) { - $payload['user'] = Str::of($server->user)->trim(); + $payload['user'] = str($server->user)->trim(); } if ($server->ip) { - $payload['ip'] = Str::of($server->ip)->trim(); + $payload['ip'] = str($server->ip)->trim(); } $server->forceFill($payload); }); @@ -462,37 +544,107 @@ public function forceDisableServer() Storage::disk('ssh-mux')->delete($this->muxFilename()); } + public function isSentinelEnabled() + { + return $this->isMetricsEnabled() || $this->isServerApiEnabled(); + } + + public function isMetricsEnabled() + { + return $this->settings->is_metrics_enabled; + } + + public function isServerApiEnabled() + { + return $this->settings->is_server_api_enabled; + } + + public function checkServerApi() + { + if ($this->isServerApiEnabled()) { + $server_ip = $this->ip; + if (isDev()) { + if ($this->id === 0) { + $server_ip = 'localhost'; + } + } + $command = "curl -s http://{$server_ip}:12172/api/health"; + $process = Process::timeout(5)->run($command); + if ($process->failed()) { + ray($process->exitCode(), $process->output(), $process->errorOutput()); + throw new \Exception("Server API is not reachable on http://{$server_ip}:12172"); + } + + } + } + public function checkSentinel() { - ray("Checking sentinel on server: {$this->name}"); - if ($this->is_metrics_enabled) { + // ray("Checking sentinel on server: {$this->name}"); + if ($this->isSentinelEnabled()) { $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); $sentinel_found = json_decode($sentinel_found, true); $status = data_get($sentinel_found, '0.State.Status', 'exited'); if ($status !== 'running') { - ray('Sentinel is not running, starting it...'); + // ray('Sentinel is not running, starting it...'); PullSentinelImageJob::dispatch($this); } else { - ray('Sentinel is running'); + // ray('Sentinel is running'); } } } - public function getMetrics() + public function getCpuMetrics(int $mins = 5) { - if ($this->is_metrics_enabled) { - $from = now()->subMinutes(5)->toIso8601ZuluString(); - $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + if ($this->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + if (str($cpu)->contains('error')) { + $error = json_decode($cpu, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } $cpu = str($cpu)->explode("\n")->skip(1)->all(); $parsedCollection = collect($cpu)->flatMap(function ($item) { return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $value] = explode(',', trim($line)); + [$time, $cpu_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 0); - return [(int) $time, (float) $value]; + return [(int) $time, (float) $cpu_usage_percent]; }); - })->toArray(); + }); - return $parsedCollection; + return $parsedCollection->toArray(); + } + } + + public function getMemoryMetrics(int $mins = 5) + { + if ($this->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); + if (str($memory)->contains('error')) { + $error = json_decode($memory, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $memory = str($memory)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($memory)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $used, $free, $usedPercent] = explode(',', trim($line)); + $usedPercent = number_format($usedPercent, 0); + + return [(int) $time, (float) $usedPercent]; + }); + }); + + return $parsedCollection->toArray(); } } @@ -806,7 +958,7 @@ public function validateOS(): bool|Stringable $releaseLines = collect(explode("\n", $os_release)); $collectedData = collect([]); foreach ($releaseLines as $line) { - $item = Str::of($line)->trim(); + $item = str($line)->trim(); $collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value()); } $ID = data_get($collectedData, 'ID'); diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 9235848ee..c39982b91 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -3,7 +3,46 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'Server Settings model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer'], + 'cleanup_after_percentage' => ['type' => 'integer'], + 'concurrent_builds' => ['type' => 'integer'], + 'dynamic_timeout' => ['type' => 'integer'], + 'force_disabled' => ['type' => 'boolean'], + 'is_build_server' => ['type' => 'boolean'], + 'is_cloudflare_tunnel' => ['type' => 'boolean'], + 'is_jump_server' => ['type' => 'boolean'], + 'is_logdrain_axiom_enabled' => ['type' => 'boolean'], + 'is_logdrain_custom_enabled' => ['type' => 'boolean'], + 'is_logdrain_highlight_enabled' => ['type' => 'boolean'], + 'is_logdrain_newrelic_enabled' => ['type' => 'boolean'], + 'is_metrics_enabled' => ['type' => 'boolean'], + 'is_reachable' => ['type' => 'boolean'], + 'is_server_api_enabled' => ['type' => 'boolean'], + 'is_swarm_manager' => ['type' => 'boolean'], + 'is_swarm_worker' => ['type' => 'boolean'], + 'is_usable' => ['type' => 'boolean'], + 'logdrain_axiom_api_key' => ['type' => 'string'], + 'logdrain_axiom_dataset_name' => ['type' => 'string'], + 'logdrain_custom_config' => ['type' => 'string'], + 'logdrain_custom_config_parser' => ['type' => 'string'], + 'logdrain_highlight_project_id' => ['type' => 'string'], + 'logdrain_newrelic_base_uri' => ['type' => 'string'], + 'logdrain_newrelic_license_key' => ['type' => 'string'], + 'metrics_history_days' => ['type' => 'integer'], + 'metrics_refresh_rate_seconds' => ['type' => 'integer'], + 'metrics_token' => ['type' => 'string'], + 'server_id' => ['type' => 'integer'], + 'wildcard_domain' => ['type' => 'string'], + 'created_at' => ['type' => 'string'], + 'updated_at' => ['type' => 'string'], + ] +)] class ServerSetting extends Model { protected $guarded = []; diff --git a/app/Models/Service.php b/app/Models/Service.php index 7851eb58a..2fc0778e6 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -6,7 +6,31 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Collection; +use OpenApi\Attributes as OA; +use Symfony\Component\Yaml\Yaml; +#[OA\Schema( + description: 'Service model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer', 'description' => 'The unique identifier of the service. Only used for database identification.'], + 'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the service.'], + 'name' => ['type' => 'string', 'description' => 'The name of the service.'], + 'environment_id' => ['type' => 'integer', 'description' => 'The unique identifier of the environment where the service is attached to.'], + 'server_id' => ['type' => 'integer', 'description' => 'The unique identifier of the server where the service is running.'], + 'description' => ['type' => 'string', 'description' => 'The description of the service.'], + 'docker_compose_raw' => ['type' => 'string', 'description' => 'The raw docker-compose.yml file of the service.'], + 'docker_compose' => ['type' => 'string', 'description' => 'The docker-compose.yml file that is parsed and modified by Coolify.'], + 'destination_id' => ['type' => 'integer', 'description' => 'The unique identifier of the destination where the service is running.'], + 'connect_to_docker_network' => ['type' => 'boolean', 'description' => 'The flag to connect the service to the predefined Docker network.'], + 'is_container_label_escape_enabled' => ['type' => 'boolean', 'description' => 'The flag to enable the container label escape.'], + 'config_hash' => ['type' => 'string', 'description' => 'The hash of the service configuration.'], + 'service_type' => ['type' => 'string', 'description' => 'The type of the service.'], + 'created_at' => ['type' => 'string', 'description' => 'The date and time when the service was created.'], + 'updated_at' => ['type' => 'string', 'description' => 'The date and time when the service was last updated.'], + 'deleted_at' => ['type' => 'string', 'description' => 'The date and time when the service was deleted.'], + ], +)] class Service extends BaseModel { use HasFactory, SoftDeletes; @@ -837,14 +861,18 @@ public function saveComposeConfigs() $commands[] = "mkdir -p $workdir"; $commands[] = "cd $workdir"; + $json = Yaml::parse($this->docker_compose); + $this->docker_compose = Yaml::dump($json, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); $docker_compose_base64 = base64_encode($this->docker_compose); + $commands[] = "echo $docker_compose_base64 | base64 -d | tee docker-compose.yml > /dev/null"; - $envs = $this->environment_variables()->get(); $commands[] = 'rm -f .env || true'; - foreach ($envs as $env) { + + $envs_from_coolify = $this->environment_variables()->get(); + foreach ($envs_from_coolify as $env) { $commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; } - if ($envs->count() === 0) { + if ($envs_from_coolify->count() === 0) { $commands[] = 'touch .env'; } instant_remote_process($commands, $this->server); @@ -859,7 +887,6 @@ public function networks() { $networks = getTopLevelNetworks($this); - // ray($networks); return $networks; } } diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index 98c1cf4e7..6690f254e 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -27,6 +27,11 @@ public function restart() instant_remote_process(["docker restart {$container_id}"], $this->service->server); } + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index c5e252c34..718fc9927 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -13,6 +13,8 @@ class StandaloneClickhouse extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'clickhouse_password' => 'encrypted', ]; @@ -178,18 +180,36 @@ public function team() return data_get($this, 'environment.project.team'); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-clickhouse'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; - } else { - return "clickhouse://{$this->clickhouse_user}:{$this->clickhouse_password}@{$this->uuid}:9000/{$this->clickhouse_db}"; - } + return new Attribute( + get: fn () => "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->uuid}:9000/{$this->clickhouse_db}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "clickhouse://{$this->clickhouse_admin_user}:{$this->clickhouse_admin_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->clickhouse_db}"; + } + + return null; + } + ); } public function environment() @@ -226,4 +246,33 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 8c739d984..b8d16d512 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -13,6 +13,8 @@ class StandaloneDragonfly extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'dragonfly_password' => 'encrypted', ]; @@ -178,18 +180,36 @@ public function portsMappingsArray(): Attribute ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-dragonfly'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; - } else { - return "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0"; - } + return new Attribute( + get: fn () => "redis://:{$this->dragonfly_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://:{$this->dragonfly_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); } public function environment() @@ -226,4 +246,33 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 5216681c9..d2963cf02 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -13,6 +13,8 @@ class StandaloneKeydb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url']; + protected $casts = [ 'keydb_password' => 'encrypted', ]; @@ -178,18 +180,36 @@ public function portsMappingsArray(): Attribute ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-keydb'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; - } else { - return "redis://{$this->keydb_password}@{$this->uuid}:6379/0"; - } + return new Attribute( + get: fn () => "redis://{$this->keydb_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://{$this->keydb_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); } public function environment() @@ -226,4 +246,33 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 33fd2cbc2..b7907f251 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -13,6 +13,8 @@ class StandaloneMariadb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'mariadb_password' => 'encrypted', ]; @@ -161,6 +163,13 @@ public function isLogDrainEnabled() return data_get($this, 'is_log_drain_enabled', false); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mariadb'; @@ -183,13 +192,24 @@ public function portsMappingsArray(): Attribute ); } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; - } else { - return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}"; - } + return new Attribute( + get: fn () => "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->uuid}:3306/{$this->mariadb_database}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mysql://{$this->mariadb_user}:{$this->mariadb_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mariadb_database}"; + } + + return null; + } + ); } public function environment() @@ -226,4 +246,33 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 0cc52b3f7..0f9f9a426 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -13,6 +13,8 @@ class StandaloneMongodb extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected static function booted() { static::created(function ($database) { @@ -198,18 +200,36 @@ public function portsMappingsArray(): Attribute ); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mongodb'; } - public function get_db_url(bool $useInternal = false) + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; - } else { - return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true"; - } + return new Attribute( + get: fn () => "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true"; + } + + return null; + } + ); } public function environment() @@ -246,4 +266,33 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 174736f77..bc4de88ee 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -13,6 +13,8 @@ class StandaloneMysql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'mysql_password' => 'encrypted', 'mysql_root_password' => 'encrypted', @@ -157,6 +159,13 @@ public function link() return null; } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-mysql'; @@ -184,13 +193,24 @@ public function portsMappingsArray(): Attribute ); } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; - } else { - return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}"; - } + return new Attribute( + get: fn () => "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->uuid}:3306/{$this->mysql_database}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "mysql://{$this->mysql_user}:{$this->mysql_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->mysql_database}"; + } + + return null; + } + ); } public function environment() @@ -227,4 +247,33 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index a5bf4dc2a..372d79fd8 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -13,6 +13,8 @@ class StandalonePostgresql extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected $casts = [ 'init_scripts' => 'array', 'postgres_password' => 'encrypted', @@ -179,18 +181,36 @@ public function team() return data_get($this, 'environment.project.team'); } + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + public function type(): string { return 'standalone-postgresql'; } - public function get_db_url(bool $useInternal = false): string + protected function internalDbUrl(): Attribute { - if ($this->is_public && ! $useInternal) { - return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; - } else { - return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}"; - } + return new Attribute( + get: fn () => "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}"; + } + + return null; + } + ); } public function environment() @@ -227,4 +247,33 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index ed379750e..64731a28b 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -13,6 +13,8 @@ class StandaloneRedis extends BaseModel protected $guarded = []; + protected $appends = ['internal_db_url', 'external_db_url', 'database_type']; + protected static function booted() { static::created(function ($database) { @@ -179,13 +181,31 @@ public function type(): string return 'standalone-redis'; } - public function get_db_url(bool $useInternal = false): string + public function databaseType(): Attribute { - if ($this->is_public && ! $useInternal) { - return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; - } else { - return "redis://:{$this->redis_password}@{$this->uuid}:6379/0"; - } + return new Attribute( + get: fn () => $this->type(), + ); + } + + protected function internalDbUrl(): Attribute + { + return new Attribute( + get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0", + ); + } + + protected function externalDbUrl(): Attribute + { + return new Attribute( + get: function () { + if ($this->is_public && $this->public_port) { + return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + } + + return null; + } + ); } public function environment() @@ -222,4 +242,33 @@ public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } + + public function getMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error == 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = str($metrics)->explode("\n")->skip(1)->all(); + $parsedCollection = collect($metrics)->flatMap(function ($item) { + return collect(explode("\n", trim($item)))->map(function ($line) { + [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); + $cpu_usage_percent = number_format($cpu_usage_percent, 2); + + return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; + }); + }); + + return $parsedCollection->toArray(); + } + } } diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index 35dc43c0c..1bd84a664 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -15,22 +15,7 @@ public function team() public function type() { - if (isLemon()) { - $basic = explode(',', config('subscription.lemon_squeezy_basic_plan_ids')); - $pro = explode(',', config('subscription.lemon_squeezy_pro_plan_ids')); - $ultimate = explode(',', config('subscription.lemon_squeezy_ultimate_plan_ids')); - - $subscription = $this->lemon_variant_id; - if (in_array($subscription, $basic)) { - return 'basic'; - } - if (in_array($subscription, $pro)) { - return 'pro'; - } - if (in_array($subscription, $ultimate)) { - return 'ultimate'; - } - } elseif (isStripe()) { + if (isStripe()) { if (! $this->stripe_plan_id) { return 'zero'; } diff --git a/app/Models/Team.php b/app/Models/Team.php index fe5995a1b..3f8e97bc5 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -7,7 +7,66 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'Team model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer', 'description' => 'The unique identifier of the team.'], + 'name' => ['type' => 'string', 'description' => 'The name of the team.'], + 'description' => ['type' => 'string', 'description' => 'The description of the team.'], + 'personal_team' => ['type' => 'boolean', 'description' => 'Whether the team is personal or not.'], + 'created_at' => ['type' => 'string', 'description' => 'The date and time the team was created.'], + 'updated_at' => ['type' => 'string', 'description' => 'The date and time the team was last updated.'], + 'smtp_enabled' => ['type' => 'boolean', 'description' => 'Whether SMTP is enabled or not.'], + 'smtp_from_address' => ['type' => 'string', 'description' => 'The email address to send emails from.'], + 'smtp_from_name' => ['type' => 'string', 'description' => 'The name to send emails from.'], + 'smtp_recipients' => ['type' => 'string', 'description' => 'The email addresses to send emails to.'], + 'smtp_host' => ['type' => 'string', 'description' => 'The SMTP host.'], + 'smtp_port' => ['type' => 'string', 'description' => 'The SMTP port.'], + 'smtp_encryption' => ['type' => 'string', 'description' => 'The SMTP encryption.'], + 'smtp_username' => ['type' => 'string', 'description' => 'The SMTP username.'], + 'smtp_password' => ['type' => 'string', 'description' => 'The SMTP password.'], + 'smtp_timeout' => ['type' => 'string', 'description' => 'The SMTP timeout.'], + 'smtp_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via SMTP.'], + 'smtp_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via SMTP.'], + 'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'], + 'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'], + 'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'], + 'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'], + 'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'], + 'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'], + 'discord_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Discord.'], + 'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'], + 'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'], + 'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'], + 'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'], + 'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'], + 'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'], + 'use_instance_email_settings' => ['type' => 'boolean', 'description' => 'Whether to use instance email settings or not.'], + 'telegram_enabled' => ['type' => 'boolean', 'description' => 'Whether Telegram is enabled or not.'], + 'telegram_token' => ['type' => 'string', 'description' => 'The Telegram token.'], + 'telegram_chat_id' => ['type' => 'string', 'description' => 'The Telegram chat ID.'], + 'telegram_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Telegram.'], + 'telegram_notifications_deployments' => ['type' => 'boolean', 'description' => 'Whether to send deployment notifications via Telegram.'], + 'telegram_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Telegram.'], + 'telegram_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Telegram.'], + 'telegram_notifications_test_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram test message thread ID.'], + 'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'], + 'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'], + 'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'], + 'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'], + 'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'], + 'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'], + 'members' => new OA\Property( + property: 'members', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/User'), + description: 'The members of the team.' + ), + ] +)] class Team extends Model implements SendsDiscord, SendsEmail { use Notifiable; diff --git a/app/Models/User.php b/app/Models/User.php index 1e120e951..18d15c0e0 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -17,7 +17,23 @@ use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\NewAccessToken; +use OpenApi\Attributes as OA; +#[OA\Schema( + description: 'User model', + type: 'object', + properties: [ + 'id' => ['type' => 'integer', 'description' => 'The user identifier in the database.'], + 'name' => ['type' => 'string', 'description' => 'The user name.'], + 'email' => ['type' => 'string', 'description' => 'The user email.'], + 'email_verified_at' => ['type' => 'string', 'description' => 'The date when the user email was verified.'], + 'created_at' => ['type' => 'string', 'description' => 'The date when the user was created.'], + 'updated_at' => ['type' => 'string', 'description' => 'The date when the user was updated.'], + 'two_factor_confirmed_at' => ['type' => 'string', 'description' => 'The date when the user two factor was confirmed.'], + 'force_password_reset' => ['type' => 'boolean', 'description' => 'The flag to force the user to reset the password.'], + 'marketing_emails' => ['type' => 'boolean', 'description' => 'The flag to receive marketing emails.'], + ], +)] class User extends Authenticatable implements SendsEmail { use HasApiTokens, HasFactory, Notifiable, TwoFactorAuthenticatable; diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 1858f31e0..a95629087 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Str; class DeploymentFailed extends Notification implements ShouldQueue { @@ -41,8 +40,8 @@ public function __construct(Application $application, string $deployment_uuid, ? $this->project_uuid = data_get($application, 'environment.project.uuid'); $this->environment_name = data_get($application, 'environment.name'); $this->fqdn = data_get($application, 'fqdn'); - if (Str::of($this->fqdn)->explode(',')->count() > 1) { - $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); + if (str($this->fqdn)->explode(',')->count() > 1) { + $this->fqdn = str($this->fqdn)->explode(',')->first(); } $this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 0cac6cbab..c06d070d8 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Str; class DeploymentSuccess extends Notification implements ShouldQueue { @@ -41,8 +40,8 @@ public function __construct(Application $application, string $deployment_uuid, ? $this->project_uuid = data_get($application, 'environment.project.uuid'); $this->environment_name = data_get($application, 'environment.name'); $this->fqdn = data_get($application, 'fqdn'); - if (Str::of($this->fqdn)->explode(',')->count() > 1) { - $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); + if (str($this->fqdn)->explode(',')->count() > 1) { + $this->fqdn = str($this->fqdn)->explode(',')->first(); } $this->deployment_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->application->uuid}/deployment/{$this->deployment_uuid}"; } diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index baf508895..72442fcb3 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -7,7 +7,6 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Str; class StatusChanged extends Notification implements ShouldQueue { @@ -31,8 +30,8 @@ public function __construct(public Application $resource) $this->project_uuid = data_get($resource, 'environment.project.uuid'); $this->environment_name = data_get($resource, 'environment.name'); $this->fqdn = data_get($resource, 'fqdn', null); - if (Str::of($this->fqdn)->explode(',')->count() > 1) { - $this->fqdn = Str::of($this->fqdn)->explode(',')->first(); + if (str($this->fqdn)->explode(',')->count() > 1) { + $this->fqdn = str($this->fqdn)->explode(',')->first(); } $this->resource_url = base_url()."/project/{$this->project_uuid}/".urlencode($this->environment_name)."/application/{$this->resource->uuid}"; } diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index a55f16a83..86c1e6e69 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -14,9 +14,7 @@ class ContainerRestarted extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public string $name, public Server $server, public ?string $url = null) - { - } + public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index d9dc57b98..75b4872cb 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -14,9 +14,7 @@ class ContainerStopped extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public string $name, public Server $server, public ?string $url = null) - { - } + public function __construct(public string $name, public Server $server, public ?string $url = null) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php index c74676eb7..90abee8a6 100644 --- a/app/Notifications/Database/DailyBackup.php +++ b/app/Notifications/Database/DailyBackup.php @@ -16,9 +16,7 @@ class DailyBackup extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public $databases) - { - } + public function __construct(public $databases) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index 6acd770f6..1d4d648c8 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -14,9 +14,7 @@ class GeneralNotification extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public string $message) - { - } + public function __construct(public string $message) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 0e445f035..f8195ec1d 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -15,9 +15,7 @@ class DockerCleanup extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server, public string $message) - { - } + public function __construct(public Server $server, public string $message) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index 960a7c79f..9a76558e2 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -17,9 +17,7 @@ class ForceDisabled extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 6a4b5d74b..a43e30376 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -17,9 +17,7 @@ class ForceEnabled extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) - { - } + public function __construct(public Server $server) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 5f63ef8f1..a6e248170 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -17,9 +17,7 @@ class HighDiskUsage extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage) - { - } + public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Revived.php index e7d3baf3e..8eaadf359 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Revived.php @@ -24,7 +24,7 @@ public function __construct(public Server $server) if ($this->server->unreachable_notification_sent === false) { return; } - GetContainersStatus::dispatch($server); + GetContainersStatus::dispatch($server)->onQueue('high'); // dispatch(new ContainerStatusJob($server)); } diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 2dcfe28b8..ebbd6af77 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -17,10 +17,7 @@ class Unreachable extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) - { - - } + public function __construct(public Server $server) {} public function via(object $notifiable): array { diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 925859aba..f873a95d3 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -13,9 +13,7 @@ class Test extends Notification implements ShouldQueue public $tries = 5; - public function __construct(public ?string $emails = null) - { - } + public function __construct(public ?string $emails = null) {} public function via(object $notifiable): array { diff --git a/app/Notifications/TransactionalEmails/InvitationLink.php b/app/Notifications/TransactionalEmails/InvitationLink.php index a251b47ea..49d2ad487 100644 --- a/app/Notifications/TransactionalEmails/InvitationLink.php +++ b/app/Notifications/TransactionalEmails/InvitationLink.php @@ -22,9 +22,7 @@ public function via(): array return [TransactionalEmailChannel::class]; } - public function __construct(public User $user) - { - } + public function __construct(public User $user) {} public function toMail(): MailMessage { diff --git a/app/Notifications/TransactionalEmails/Test.php b/app/Notifications/TransactionalEmails/Test.php index ed30c1883..a417e1ee5 100644 --- a/app/Notifications/TransactionalEmails/Test.php +++ b/app/Notifications/TransactionalEmails/Test.php @@ -14,9 +14,7 @@ class Test extends Notification implements ShouldQueue public $tries = 5; - public function __construct(public string $emails) - { - } + public function __construct(public string $emails) {} public function via(): array { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1bce22c12..6822dec13 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -9,9 +9,7 @@ class AppServiceProvider extends ServiceProvider { - public function register(): void - { - } + public function register(): void {} public function boot(): void { diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php index 0c6422f0c..9b58882eb 100644 --- a/app/Traits/ExecuteRemoteCommand.php +++ b/app/Traits/ExecuteRemoteCommand.php @@ -7,7 +7,6 @@ use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Process; -use Illuminate\Support\Str; trait ExecuteRemoteCommand { @@ -45,7 +44,7 @@ public function execute_remote_command(...$commands) } $remote_command = generateSshCommand($this->server, $command); $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) { - $output = Str::of($output)->trim(); + $output = str($output)->trim(); if ($output->startsWith('╔')) { $output = "\n".$output; } diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index 36c07dae1..35448d5e5 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -22,8 +22,7 @@ public function __construct( public bool $allowToPeak = true, public bool $isMultiline = false, public string $defaultClass = 'input', - ) { - } + ) {} public function render(): View|Closure|string { diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index bfdf03a31..7d1860500 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -19,6 +19,8 @@ public function __construct( public ?string $value = null, public ?string $label = null, public ?string $placeholder = null, + public ?string $monacoEditorLanguage = '', + public bool $useMonacoEditor = false, public bool $required = false, public bool $disabled = false, public bool $readonly = false, diff --git a/app/View/Components/ResourceView.php b/app/View/Components/ResourceView.php index 5a11b159d..d1107465b 100644 --- a/app/View/Components/ResourceView.php +++ b/app/View/Components/ResourceView.php @@ -16,9 +16,7 @@ public function __construct( public ?string $logo = null, public ?string $documentation = null, public bool $upgrade = false, - ) { - - } + ) {} /** * Get the view / contents that represent the component. diff --git a/app/View/Components/Services/Links.php b/app/View/Components/Services/Links.php index 9baf0578d..0497aebae 100644 --- a/app/View/Components/Services/Links.php +++ b/app/View/Components/Services/Links.php @@ -6,7 +6,6 @@ use Closure; use Illuminate\Contracts\View\View; use Illuminate\Support\Collection; -use Illuminate\Support\Str; use Illuminate\View\Component; class Links extends Component @@ -26,16 +25,16 @@ public function __construct(public Service $service) $this->links = $this->links->merge($links); } else { if ($application->fqdn) { - $fqdns = collect(Str::of($application->fqdn)->explode(',')); + $fqdns = collect(str($application->fqdn)->explode(',')); $fqdns->map(function ($fqdn) { $this->links->push(getFqdnWithoutPort($fqdn)); }); } if ($application->ports) { - $portsCollection = collect(Str::of($application->ports)->explode(',')); + $portsCollection = collect(str($application->ports)->explode(',')); $portsCollection->map(function ($port) { - if (Str::of($port)->contains(':')) { - $hostPort = Str::of($port)->before(':'); + if (str($port)->contains(':')) { + $hostPort = str($port)->before(':'); } else { $hostPort = $port; } diff --git a/app/View/Components/Status/Index.php b/app/View/Components/Status/Index.php index f8436a102..ada9eb682 100644 --- a/app/View/Components/Status/Index.php +++ b/app/View/Components/Status/Index.php @@ -14,8 +14,7 @@ class Index extends Component public function __construct( public $resource = null, public bool $showRefreshButton = true, - ) { - } + ) {} /** * Get the view / contents that represent the component. diff --git a/bootstrap/helpers/api.php b/bootstrap/helpers/api.php index 999de45c2..8e14ef9ee 100644 --- a/bootstrap/helpers/api.php +++ b/bootstrap/helpers/api.php @@ -1,12 +1,178 @@ user()->currentAccessToken(); return data_get($token, 'team_id'); } -function invalid_token() +function invalidTokenResponse() { - return response()->json(['error' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); + return response()->json(['message' => 'Invalid token.', 'docs' => 'https://coolify.io/docs/api-reference/authorization'], 400); +} + +function serializeApiResponse($data) +{ + if ($data instanceof Collection) { + $data = $data->map(function ($d) { + $d = collect($d)->sortKeys(); + $created_at = data_get($d, 'created_at'); + $updated_at = data_get($d, 'updated_at'); + if ($created_at) { + unset($d['created_at']); + $d['created_at'] = $created_at; + + } + if ($updated_at) { + unset($d['updated_at']); + $d['updated_at'] = $updated_at; + } + if (data_get($d, 'name')) { + $d = $d->prepend($d['name'], 'name'); + } + if (data_get($d, 'description')) { + $d = $d->prepend($d['description'], 'description'); + } + if (data_get($d, 'uuid')) { + $d = $d->prepend($d['uuid'], 'uuid'); + } + + if (! is_null(data_get($d, 'id'))) { + $d = $d->prepend($d['id'], 'id'); + } + + return $d; + }); + + return $data; + } else { + $d = collect($data)->sortKeys(); + $created_at = data_get($d, 'created_at'); + $updated_at = data_get($d, 'updated_at'); + if ($created_at) { + unset($d['created_at']); + $d['created_at'] = $created_at; + + } + if ($updated_at) { + unset($d['updated_at']); + $d['updated_at'] = $updated_at; + } + if (data_get($d, 'name')) { + $d = $d->prepend($d['name'], 'name'); + } + if (data_get($d, 'description')) { + $d = $d->prepend($d['description'], 'description'); + } + if (data_get($d, 'uuid')) { + $d = $d->prepend($d['uuid'], 'uuid'); + } + + if (! is_null(data_get($d, 'id'))) { + $d = $d->prepend($d['id'], 'id'); + } + + return $d; + } +} + +function sharedDataApplications() +{ + return [ + 'git_repository' => 'string', + 'git_branch' => 'string', + 'build_pack' => Rule::enum(BuildPackTypes::class), + 'is_static' => 'boolean', + 'domains' => 'string', + 'redirect' => Rule::enum(RedirectTypes::class), + 'git_commit_sha' => 'string', + 'docker_registry_image_name' => 'string|nullable', + 'docker_registry_image_tag' => 'string|nullable', + 'install_command' => 'string|nullable', + 'build_command' => 'string|nullable', + 'start_command' => 'string|nullable', + 'ports_exposes' => 'string|regex:/^(\d+)(,\d+)*$/', + 'ports_mappings' => 'string|regex:/^(\d+:\d+)(,\d+:\d+)*$/|nullable', + 'base_directory' => 'string|nullable', + 'publish_directory' => 'string|nullable', + 'health_check_enabled' => 'boolean', + 'health_check_path' => 'string', + 'health_check_port' => 'string|nullable', + 'health_check_host' => 'string', + 'health_check_method' => 'string', + 'health_check_return_code' => 'numeric', + 'health_check_scheme' => 'string', + 'health_check_response_text' => 'string|nullable', + 'health_check_interval' => 'numeric', + 'health_check_timeout' => 'numeric', + 'health_check_retries' => 'numeric', + 'health_check_start_period' => 'numeric', + 'limits_memory' => 'string', + 'limits_memory_swap' => 'string', + 'limits_memory_swappiness' => 'numeric', + 'limits_memory_reservation' => 'string', + 'limits_cpus' => 'string', + 'limits_cpuset' => 'string|nullable', + 'limits_cpu_shares' => 'numeric', + 'custom_labels' => 'string|nullable', + 'custom_docker_run_options' => 'string|nullable', + 'post_deployment_command' => 'string|nullable', + 'post_deployment_command_container' => 'string', + 'pre_deployment_command' => 'string|nullable', + 'pre_deployment_command_container' => 'string', + 'manual_webhook_secret_github' => 'string|nullable', + 'manual_webhook_secret_gitlab' => 'string|nullable', + 'manual_webhook_secret_bitbucket' => 'string|nullable', + 'manual_webhook_secret_gitea' => 'string|nullable', + 'docker_compose_location' => 'string', + 'docker_compose' => 'string|nullable', + 'docker_compose_raw' => 'string|nullable', + 'docker_compose_domains' => 'array|nullable', + 'docker_compose_custom_start_command' => 'string|nullable', + 'docker_compose_custom_build_command' => 'string|nullable', + ]; +} + +function validateIncomingRequest(Request $request) +{ + // check if request is json + if (! $request->isJson()) { + return response()->json([ + 'message' => 'Invalid request.', + 'error' => 'Content-Type must be application/json.', + ], 400); + } + // check if request is valid json + if (! json_decode($request->getContent())) { + return response()->json([ + 'message' => 'Invalid request.', + 'error' => 'Invalid JSON.', + ], 400); + } + // check if valid json is empty + if (empty($request->json()->all())) { + return response()->json([ + 'message' => 'Invalid request.', + 'error' => 'Empty JSON.', + ], 400); + } +} + +function removeUnnecessaryFieldsFromRequest(Request $request) +{ + $request->offsetUnset('project_uuid'); + $request->offsetUnset('environment_name'); + $request->offsetUnset('destination_uuid'); + $request->offsetUnset('server_uuid'); + $request->offsetUnset('type'); + $request->offsetUnset('domains'); + $request->offsetUnset('instant_deploy'); + $request->offsetUnset('github_app_uuid'); + $request->offsetUnset('private_key_uuid'); } diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index 376b0f2aa..1a08a46eb 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -8,7 +8,7 @@ use App\Models\StandaloneDocker; use Spatie\Url\Url; -function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) +function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false) { $application_id = $application->id; $deployment_link = Url::fromString($application->link()."/deployment/{$deployment_uuid}"); @@ -35,6 +35,7 @@ function queue_application_deployment(Application $application, string $deployme 'pull_request_id' => $pull_request_id, 'force_rebuild' => $force_rebuild, 'is_webhook' => $is_webhook, + 'is_api' => $is_api, 'restart_only' => $restart_only, 'commit' => $commit, 'rollback' => $rollback, @@ -45,11 +46,11 @@ function queue_application_deployment(Application $application, string $deployme if ($no_questions_asked) { dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, - )); + ))->onQueue('high'); } elseif (next_queuable($server_id, $application_id)) { dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, - )); + ))->onQueue('high'); } } function force_start_deployment(ApplicationDeploymentQueue $deployment) @@ -60,12 +61,12 @@ function force_start_deployment(ApplicationDeploymentQueue $deployment) dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $deployment->id, - )); + ))->onQueue('high'); } function queue_next_deployment(Application $application) { $server_id = $application->destination->server_id; - $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', 'queued')->get()->sortBy('created_at')->first(); + $next_found = ApplicationDeploymentQueue::where('server_id', $server_id)->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at')->first(); if ($next_found) { $next_found->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, @@ -73,13 +74,13 @@ function queue_next_deployment(Application $application) dispatch(new ApplicationDeploymentJob( application_deployment_queue_id: $next_found->id, - )); + ))->onQueue('high'); } } function next_queuable(string $server_id, string $application_id): bool { - $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', 'queued'])->get()->sortByDesc('created_at'); + $deployments = ApplicationDeploymentQueue::where('server_id', $server_id)->whereIn('status', ['in_progress', ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); $same_application_deployments = $deployments->where('application_id', $application_id); $in_progress = $same_application_deployments->filter(function ($value, $key) { return $value->status === 'in_progress'; @@ -98,3 +99,26 @@ function next_queuable(string $server_id, string $application_id): bool return true; } +function next_after_cancel(?Server $server = null) +{ + if ($server) { + $next_found = ApplicationDeploymentQueue::where('server_id', data_get($server, 'id'))->where('status', ApplicationDeploymentStatus::QUEUED)->get()->sortBy('created_at'); + if ($next_found->count() > 0) { + foreach ($next_found as $next) { + $server = Server::find($next->server_id); + $concurrent_builds = $server->settings->concurrent_builds; + $inprogress_deployments = ApplicationDeploymentQueue::where('server_id', $next->server_id)->whereIn('status', [ApplicationDeploymentStatus::QUEUED])->get()->sortByDesc('created_at'); + if ($inprogress_deployments->count() < $concurrent_builds) { + $next->update([ + 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, + ]); + + dispatch(new ApplicationDeploymentJob( + application_deployment_queue_id: $next->id, + ))->onQueue('high'); + } + break; + } + } + } +} diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index dba8aa543..089298956 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -19,136 +19,165 @@ function generate_database_name(string $type): string return $type.'-database-'.$cuid; } -function create_standalone_postgresql($environment_id, $destination_uuid): StandalonePostgresql +function create_standalone_postgresql($environmentId, $destinationUuid, ?array $otherData = null): StandalonePostgresql { - // TODO: If another type of destination is added, this will need to be updated. - $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); + $destination = StandaloneDocker::where('uuid', $destinationUuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandalonePostgresql(); + $database->name = generate_database_name('postgresql'); + $database->postgres_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environmentId; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandalonePostgresql::create([ - 'name' => generate_database_name('postgresql'), - 'postgres_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_redis($environment_id, $destination_uuid): StandaloneRedis +function create_standalone_redis($environment_id, $destination_uuid, ?array $otherData = null): StandaloneRedis { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneRedis(); + $database->name = generate_database_name('redis'); + $database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneRedis::create([ - 'name' => generate_database_name('redis'), - 'redis_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mongodb($environment_id, $destination_uuid): StandaloneMongodb +function create_standalone_mongodb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMongodb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMongodb(); + $database->name = generate_database_name('mongodb'); + $database->mongo_initdb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMongodb::create([ - 'name' => generate_database_name('mongodb'), - 'mongo_initdb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mysql($environment_id, $destination_uuid): StandaloneMysql +function create_standalone_mysql($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMysql { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMysql(); + $database->name = generate_database_name('mysql'); + $database->mysql_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mysql_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneMysql::create([ - 'name' => generate_database_name('mysql'), - 'mysql_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mysql_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_mariadb($environment_id, $destination_uuid): StandaloneMariadb +function create_standalone_mariadb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneMariadb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneMariadb(); + $database->name = generate_database_name('mariadb'); + $database->mariadb_root_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->mariadb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); - return StandaloneMariadb::create([ - 'name' => generate_database_name('mariadb'), - 'mariadb_root_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'mariadb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); + + return $database; } -function create_standalone_keydb($environment_id, $destination_uuid): StandaloneKeydb +function create_standalone_keydb($environment_id, $destination_uuid, ?array $otherData = null): StandaloneKeydb { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneKeydb(); + $database->name = generate_database_name('keydb'); + $database->keydb_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneKeydb::create([ - 'name' => generate_database_name('keydb'), - 'keydb_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_dragonfly($environment_id, $destination_uuid): StandaloneDragonfly +function create_standalone_dragonfly($environment_id, $destination_uuid, ?array $otherData = null): StandaloneDragonfly { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneDragonfly(); + $database->name = generate_database_name('dragonfly'); + $database->dragonfly_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneDragonfly::create([ - 'name' => generate_database_name('dragonfly'), - 'dragonfly_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -function create_standalone_clickhouse($environment_id, $destination_uuid): StandaloneClickhouse +function create_standalone_clickhouse($environment_id, $destination_uuid, ?array $otherData = null): StandaloneClickhouse { $destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); if (! $destination) { throw new Exception('Destination not found'); } + $database = new StandaloneClickhouse(); + $database->name = generate_database_name('clickhouse'); + $database->clickhouse_admin_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $database->environment_id = $environment_id; + $database->destination_id = $destination->id; + $database->destination_type = $destination->getMorphClass(); + if ($otherData) { + $database->fill($otherData); + } + $database->save(); - return StandaloneClickhouse::create([ - 'name' => generate_database_name('clickhouse'), - 'clickhouse_admin_password' => \Illuminate\Support\Str::password(length: 64, symbols: false), - 'environment_id' => $environment_id, - 'destination_id' => $destination->id, - 'destination_type' => $destination->getMorphClass(), - ]); + return $database; } -/** - * Delete file locally on the filesystem. - */ function delete_backup_locally(?string $filename, Server $server): void { if (empty($filename)) { @@ -156,3 +185,17 @@ function delete_backup_locally(?string $filename, Server $server): void } instant_remote_process(["rm -f \"{$filename}\""], $server, throwError: false); } + +function isPublicPortAlreadyUsed(Server $server, int $port, ?string $id = null): bool +{ + if ($id) { + $foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->where('id', '!=', $id)->first(); + } else { + $foundDatabase = $server->databases()->where('public_port', $port)->where('is_public', true)->first(); + } + if ($foundDatabase) { + return true; + } + + return false; +} diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 91e553cf6..21e946a9a 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -91,7 +91,7 @@ function format_docker_envs_to_json($rawOutput) } function checkMinimumDockerEngineVersion($dockerVersion) { - $majorDockerVersion = Str::of($dockerVersion)->before('.')->value(); + $majorDockerVersion = str($dockerVersion)->before('.')->value(); if ($majorDockerVersion <= 22) { $dockerVersion = null; } @@ -152,7 +152,7 @@ function get_port_from_dockerfile($dockerfile): ?int $dockerfile_array = explode("\n", $dockerfile); $found_exposed_port = null; foreach ($dockerfile_array as $line) { - $line_str = Str::of($line)->trim(); + $line_str = str($line)->trim(); if ($line_str->startsWith('EXPOSE')) { $found_exposed_port = $line_str->replace('EXPOSE', '')->trim(); break; @@ -458,7 +458,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares = collect([]); if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$http_label}-stripprefix.stripprefix.prefixes={$path}"); - $middlewares->push("{$https_label}-stripprefix"); + $middlewares->push("{$http_label}-stripprefix"); } if ($is_gzip_enabled) { $middlewares->push('gzip'); @@ -534,7 +534,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview $labels = collect([]); if ($pull_request_id === 0) { if ($application->fqdn) { - $domains = Str::of(data_get($application, 'fqdn'))->explode(','); + $domains = str(data_get($application, 'fqdn'))->explode(','); $labels = $labels->merge(fqdnLabelsForTraefik( uuid: $appUuid, domains: $domains, @@ -558,7 +558,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview } } else { if (data_get($preview, 'fqdn')) { - $domains = Str::of(data_get($preview, 'fqdn'))->explode(','); + $domains = str(data_get($preview, 'fqdn'))->explode(','); } else { $domains = collect([]); } diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index d916dc9c8..98e405c74 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -85,7 +85,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m function get_installation_path(GithubApp $source) { $github = GithubApp::where('uuid', $source->uuid)->first(); - $name = Str::of(Str::kebab($github->name)); + $name = str(Str::kebab($github->name)); $installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps'; return "$github->html_url/$installation_path/$name/installations/new"; @@ -93,7 +93,7 @@ function get_installation_path(GithubApp $source) function get_permissions_path(GithubApp $source) { $github = GithubApp::where('uuid', $source->uuid)->first(); - $name = Str::of(Str::kebab($github->name)); + $name = str(Str::kebab($github->name)); return "$github->html_url/settings/apps/$name/permissions"; } diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index 0cc4c51e7..9140ca8c8 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -4,7 +4,6 @@ use App\Models\EnvironmentVariable; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; -use Illuminate\Support\Str; use Spatie\Url\Url; use Symfony\Component\Yaml\Yaml; @@ -38,7 +37,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli ]); instant_remote_process($commands, $server); foreach ($fileVolumes as $fileVolume) { - $path = Str::of(data_get($fileVolume, 'fs_path')); + $path = str(data_get($fileVolume, 'fs_path')); $content = data_get($fileVolume, 'content'); if ($path->startsWith('.')) { $path = $path->after('.'); @@ -68,7 +67,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli $fileVolume->is_directory = false; $fileVolume->save(); $content = base64_encode($content); - $dir = Str::of($fileLocation)->dirname(); + $dir = str($fileLocation)->dirname(); instant_remote_process([ "mkdir -p $dir", "echo '$content' | base64 -d | tee $fileLocation", @@ -106,7 +105,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $resourceFqdns = str($resource->fqdn)->explode(','); if ($resourceFqdns->count() === 1) { $resourceFqdns = $resourceFqdns->first(); - $variableName = 'SERVICE_FQDN_'.Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $fqdn = Url::fromString($resourceFqdns); $port = $fqdn->getPort(); @@ -125,14 +124,14 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $generatedEnv->save(); } } - $variableName = 'SERVICE_URL_'.Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $url = Url::fromString($fqdn); $port = $url->getPort(); $path = $url->getPath(); $url = $url->getHost(); if ($generatedEnv) { - $url = Str::of($fqdn)->after('://'); + $url = str($fqdn)->after('://'); $generatedEnv->value = $url.$path; $generatedEnv->save(); } @@ -175,7 +174,7 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $port_env_url->save(); } } else { - $variableName = 'SERVICE_FQDN_'.Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_FQDN_'.str($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $fqdn = Url::fromString($fqdn); $fqdn = $fqdn->getScheme().'://'.$fqdn->getHost().$fqdn->getPath(); @@ -183,12 +182,12 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) $generatedEnv->value = $fqdn; $generatedEnv->save(); } - $variableName = 'SERVICE_URL_'.Str::of($resource->name)->upper()->replace('-', ''); + $variableName = 'SERVICE_URL_'.str($resource->name)->upper()->replace('-', ''); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $url = Url::fromString($fqdn); $url = $url->getHost().$url->getPath(); if ($generatedEnv) { - $url = Str::of($fqdn)->after('://'); + $url = str($fqdn)->after('://'); $generatedEnv->value = $url; $generatedEnv->save(); } @@ -200,3 +199,10 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) return handleError($e); } } +function serviceKeys() +{ + $services = get_service_templates(); + $serviceKeys = $services->keys(); + + return $serviceKeys; +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7994c10af..8b7422ad4 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -40,6 +40,7 @@ use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Support\Stringable; use Lcobucci\JWT\Encoding\ChainedFormatter; @@ -79,6 +80,10 @@ function backup_dir(): string { return base_configuration_dir().'/backups'; } +function metrics_dir(): string +{ + return base_configuration_dir().'/metrics'; +} function generate_readme_file(string $name, string $updated_at): string { @@ -158,10 +163,10 @@ function get_route_parameters(): array function get_latest_sentinel_version(): string { try { - $response = Http::get('https://cdn.coollabs.io/coolify/versions.json'); + $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json'); $versions = $response->json(); - return data_get($versions, 'coolify.sentinel.version'); + return data_get($versions, 'sentinel.version'); } catch (\Throwable $e) { //throw $e; ray($e->getMessage()); @@ -465,7 +470,7 @@ function data_get_str($data, $key, $default = null): Stringable { $str = data_get($data, $key, $default) ?? $default; - return Str::of($str); + return str($str); } function generateFqdn(Server $server, string $random) @@ -531,6 +536,43 @@ function getResourceByUuid(string $uuid, ?int $teamId = null) return null; } +function queryDatabaseByUuidWithinTeam(string $uuid, string $teamId) +{ + $postgresql = StandalonePostgresql::whereUuid($uuid)->first(); + if ($postgresql && $postgresql->team()->id == $teamId) { + return $postgresql->unsetRelation('environment')->unsetRelation('destination'); + } + $redis = StandaloneRedis::whereUuid($uuid)->first(); + if ($redis && $redis->team()->id == $teamId) { + return $redis->unsetRelation('environment'); + } + $mongodb = StandaloneMongodb::whereUuid($uuid)->first(); + if ($mongodb && $mongodb->team()->id == $teamId) { + return $mongodb->unsetRelation('environment'); + } + $mysql = StandaloneMysql::whereUuid($uuid)->first(); + if ($mysql && $mysql->team()->id == $teamId) { + return $mysql->unsetRelation('environment'); + } + $mariadb = StandaloneMariadb::whereUuid($uuid)->first(); + if ($mariadb && $mariadb->team()->id == $teamId) { + return $mariadb->unsetRelation('environment'); + } + $keydb = StandaloneKeydb::whereUuid($uuid)->first(); + if ($keydb && $keydb->team()->id == $teamId) { + return $keydb->unsetRelation('environment'); + } + $dragonfly = StandaloneDragonfly::whereUuid($uuid)->first(); + if ($dragonfly && $dragonfly->team()->id == $teamId) { + return $dragonfly->unsetRelation('environment'); + } + $clickhouse = StandaloneClickhouse::whereUuid($uuid)->first(); + if ($clickhouse && $clickhouse->team()->id == $teamId) { + return $clickhouse->unsetRelation('environment'); + } + + return null; +} function queryResourcesByUuid(string $uuid) { $resource = null; @@ -929,12 +971,12 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $content = null; $isDirectory = false; if (is_string($volume)) { - $source = Str::of($volume)->before(':'); - $target = Str::of($volume)->after(':')->beforeLast(':'); + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); if ($source->startsWith('./') || $source->startsWith('/') || $source->startsWith('~')) { - $type = Str::of('bind'); + $type = str('bind'); } else { - $type = Str::of('volume'); + $type = str('volume'); } } elseif (is_array($volume)) { $type = data_get_str($volume, 'type'); @@ -983,8 +1025,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $slugWithoutUuid = Str::slug($source, '-'); $name = "{$savedService->service->uuid}_{$slugWithoutUuid}"; if (is_string($volume)) { - $source = Str::of($volume)->before(':'); - $target = Str::of($volume)->after(':')->beforeLast(':'); + $source = str($volume)->before(':'); + $target = str($volume)->after(':')->beforeLast(':'); $source = $name; $volume = "$source:$target"; } elseif (is_array($volume)) { @@ -1028,7 +1070,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // Get variables from the service foreach ($serviceVariables as $variableName => $variable) { if (is_numeric($variableName)) { - $variable = Str::of($variable); + $variable = str($variable); if ($variable->contains('=')) { // - SESSION_SECRET=123 // - SESSION_SECRET= @@ -1042,8 +1084,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { // SESSION_SECRET: 123 // SESSION_SECRET: - $key = Str::of($variableName); - $value = Str::of($variable); + $key = str($variableName); + $value = str($variable); } if ($key->startsWith('SERVICE_FQDN')) { if ($isNew || $savedService->fqdn === null) { @@ -1133,7 +1175,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'key' => $key, 'service_id' => $resource->id, ])->first(); - $value = Str::of(replaceVariables($value)); + $value = str(replaceVariables($value)); $key = $value; if ($value->startsWith('SERVICE_')) { $foundEnv = EnvironmentVariable::where([ @@ -1166,7 +1208,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // } } else { if ($command->value() === 'URL') { - $fqdn = Str::of($fqdn)->after('://')->value(); + $fqdn = str($fqdn)->after('://')->value(); } EnvironmentVariable::create([ 'key' => $key, @@ -1314,21 +1356,44 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal data_forget($service, 'volumes.*.isDirectory'); data_forget($service, 'volumes.*.is_directory'); data_forget($service, 'exclude_from_hc'); - - // Remove unnecessary variables from service.environment - // $withoutServiceEnvs = collect([]); - // collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) { - // ray($key, $value); - // if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) { - // $k = Str::of($value)->before("="); - // $v = Str::of($value)->after("="); - // $withoutServiceEnvs->put($k->value(), $v->value()); - // } - // }); - // ray($withoutServiceEnvs); - // data_set($service, 'environment', $withoutServiceEnvs->toArray()); + data_set($service, 'environment', $serviceVariables->toArray()); updateCompose($savedService); + return $service; + + }); + + $envs_from_coolify = $resource->environment_variables()->get(); + $services = collect($services)->map(function ($service, $serviceName) use ($resource, $envs_from_coolify) { + $serviceVariables = collect(data_get($service, 'environment', [])); + $parsedServiceVariables = collect([]); + foreach ($serviceVariables as $key => $value) { + if (is_numeric($key)) { + $value = str($value); + if ($value->contains('=')) { + $key = $value->before('=')->value(); + $value = $value->after('=')->value(); + } else { + $key = $value->value(); + $value = null; + } + $parsedServiceVariables->put($key, $value); + } else { + $parsedServiceVariables->put($key, $value); + } + } + $parsedServiceVariables->put('COOLIFY_CONTAINER_NAME', "$serviceName-{$resource->uuid}"); + $parsedServiceVariables = $parsedServiceVariables->map(function ($value, $key) use ($envs_from_coolify) { + $found_env = $envs_from_coolify->where('key', $key)->first(); + if ($found_env) { + return $found_env->value; + } + + return $value; + }); + + data_set($service, 'environment', $parsedServiceVariables->toArray()); + return $service; }); $finalServices = [ @@ -1633,7 +1698,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal // Get variables from the service foreach ($serviceVariables as $variableName => $variable) { if (is_numeric($variableName)) { - $variable = Str::of($variable); + $variable = str($variable); if ($variable->contains('=')) { // - SESSION_SECRET=123 // - SESSION_SECRET= @@ -1647,8 +1712,8 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { // SESSION_SECRET: 123 // SESSION_SECRET: - $key = Str::of($variableName); - $value = Str::of($variable); + $key = str($variableName); + $value = str($variable); } if ($key->startsWith('SERVICE_FQDN')) { if ($isNew) { @@ -1692,7 +1757,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'application_id' => $resource->id, 'is_preview' => false, ])->first(); - $value = Str::of(replaceVariables($value)); + $value = str(replaceVariables($value)); $key = $value; if ($value->startsWith('SERVICE_')) { $foundEnv = EnvironmentVariable::where([ @@ -1714,7 +1779,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal $fqdn = data_get($foundEnv, 'value'); } else { if ($command?->value() === 'URL') { - $fqdn = Str::of($fqdn)->after('://')->value(); + $fqdn = str($fqdn)->after('://')->value(); } EnvironmentVariable::create([ 'key' => $key, @@ -1880,8 +1945,6 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal 'networks' => $topLevelNetworks->toArray(), ]; if ($isSameDockerComposeFile) { - $resource->docker_compose_pr_raw = Yaml::dump($yaml, 10, 2); - $resource->docker_compose_pr = Yaml::dump($finalServices, 10, 2); $resource->docker_compose_raw = Yaml::dump($yaml, 10, 2); $resource->docker_compose = Yaml::dump($finalServices, 10, 2); } else { @@ -2103,6 +2166,75 @@ function ip_match($ip, $cidrs, &$match = null) return false; } +function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId, string $uuid) +{ + if (is_null($teamId)) { + return response()->json(['error' => 'Team ID is required.'], 400); + } + if (is_array($domains)) { + $domains = collect($domains); + } + + $domains = $domains->map(function ($domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + + return str($domain); + }); + $applications = Application::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid'])->filter(fn ($app) => $app->uuid !== $uuid); + $serviceApplications = ServiceApplication::ownedByCurrentTeamAPI($teamId)->get(['fqdn', 'uuid'])->filter(fn ($app) => $app->uuid !== $uuid); + $domainFound = false; + foreach ($applications as $app) { + if (is_null($app->fqdn)) { + continue; + } + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $domainFound = true; + break; + } + } + } + if ($domainFound) { + return true; + } + foreach ($serviceApplications as $app) { + if (str($app->fqdn)->isEmpty()) { + continue; + } + $list_of_domains = collect(explode(',', $app->fqdn))->filter(fn ($fqdn) => $fqdn !== ''); + foreach ($list_of_domains as $domain) { + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + $domainFound = true; + break; + } + } + } + if ($domainFound) { + return true; + } + $settings = InstanceSettings::get(); + if (data_get($settings, 'fqdn')) { + $domain = data_get($settings, 'fqdn'); + if (str($domain)->endsWith('/')) { + $domain = str($domain)->beforeLast('/'); + } + $naked_domain = str($domain)->value(); + if ($domains->contains($naked_domain)) { + return true; + } + } +} function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { if ($resource) { @@ -2282,3 +2414,25 @@ function isAnyDeploymentInprogress() echo "No deployments in progress.\n"; exit(0); } + +function generateSentinelToken() +{ + $token = Str::random(64); + + return $token; +} + +function isBase64Encoded($strValue) +{ + return base64_encode(base64_decode($strValue, true)) === $strValue; +} +function customApiValidator(Collection|array $item, array $rules) +{ + if (is_array($item)) { + $item = collect($item); + } + + return Validator::make($item->toArray(), $rules, [ + 'required' => 'This field is required.', + ]); +} diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index 224a65f0a..aadd2dd34 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -1,51 +1,8 @@ user()->id; - $team_id = currentTeam()->id ?? null; - $email = auth()->user()->email ?? null; - $name = auth()->user()->name ?? null; - $url = "https://store.coollabs.io/checkout/buy/$checkout_id?"; - if ($user_id) { - $url .= "&checkout[custom][user_id]={$user_id}"; - } - if (isset($team_id)) { - $url .= "&checkout[custom][team_id]={$team_id}"; - } - if ($email) { - $url .= "&checkout[email]={$email}"; - } - if ($name) { - $url .= "&checkout[name]={$name}"; - } - - return $url; -} - -function getPaymentLink() -{ - return currentTeam()->subscription->lemon_update_payment_menthod_url; -} - -function getRenewDate() -{ - return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s'); -} - -function getEndDate() -{ - return Carbon::parse(currentTeam()->subscription->lemon_renews_at)->format('Y-M-d H:i:s'); -} - function isSubscriptionActive() { if (! isCloud()) { @@ -60,12 +17,6 @@ function isSubscriptionActive() if (is_null($subscription)) { return false; } - if (isLemon()) { - return $subscription->lemon_status === 'active'; - } - // if (isPaddle()) { - // return $subscription->paddle_status === 'active'; - // } if (isStripe()) { return $subscription->stripe_invoice_paid === true; } @@ -82,12 +33,6 @@ function isSubscriptionOnGracePeriod() if (! $subscription) { return false; } - if (isLemon()) { - $is_still_grace_period = $subscription->lemon_ends_at && - Carbon::parse($subscription->lemon_ends_at) > Carbon::now(); - - return $is_still_grace_period; - } if (isStripe()) { return $subscription->stripe_cancel_at_period_end; } @@ -98,18 +43,10 @@ function subscriptionProvider() { return config('subscription.provider'); } -function isLemon() -{ - return config('subscription.provider') === 'lemon'; -} function isStripe() { return config('subscription.provider') === 'stripe'; } -function isPaddle() -{ - return config('subscription.provider') === 'paddle'; -} function getStripeCustomerPortalSession(Team $team) { Stripe::setApiKey(config('subscription.stripe_api_key')); diff --git a/composer.json b/composer.json index b49f9668a..b5aed62dc 100644 --- a/composer.json +++ b/composer.json @@ -13,17 +13,18 @@ "doctrine/dbal": "^3.6", "guzzlehttp/guzzle": "^7.5.0", "laravel/fortify": "^v1.16.0", - "laravel/framework": "^v10.7.1", + "laravel/framework": "^v11", "laravel/horizon": "^5.23.1", "laravel/prompts": "^0.1.6", - "laravel/sanctum": "^v3.2.1", + "laravel/sanctum": "^v4.0", "laravel/socialite": "^v5.14.0", "laravel/tinker": "^v2.8.1", "laravel/ui": "^4.2", "lcobucci/jwt": "^5.0.0", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-sftp-v3": "^3.0", - "livewire/livewire": "3.4.9", + "livewire/livewire": "^3.5", + "log1x/laravel-webfonts": "^1.0", "lorisleiva/laravel-actions": "^2.7", "nubs/random-name-generator": "^2.2", "phpseclib/phpseclib": "~3.0", @@ -31,8 +32,7 @@ "poliander/cron": "^3.0", "purplepixie/phpdns": "^2.1", "pusher/pusher-php-server": "^7.2", - "resend/resend-laravel": "^0.5.0", - "sentry/sentry-laravel": "^3.4", + "sentry/sentry-laravel": "^4.6", "socialiteproviders/microsoft-azure": "^5.1", "spatie/laravel-activitylog": "^4.7.3", "spatie/laravel-data": "^3.4.3", @@ -42,14 +42,15 @@ "stripe/stripe-php": "^12.0", "symfony/yaml": "^6.2", "visus/cuid2": "^2.0.0", - "yosymfony/toml": "^1.0" + "yosymfony/toml": "^1.0", + "zircote/swagger-php": "^4.10" }, "require-dev": { "fakerphp/faker": "^v1.21.0", - "laravel/dusk": "^v7.7.0", + "laravel/dusk": "^v8.0", "laravel/pint": "^1.16", "mockery/mockery": "^1.5.1", - "nunomaduro/collision": "^v7.4.0", + "nunomaduro/collision": "^v8.1", "pestphp/pest": "^2.16", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.0.19", @@ -105,4 +106,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 9d04e9ec7..3923bc5bd 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": "dbce9f366320f4d58392673fe25c69f6", + "content-hash": "31dfd250055a26ee8fe8089fe5d308ac", "packages": [ { "name": "amphp/amp", @@ -229,16 +229,16 @@ }, { "name": "amphp/dns", - "version": "v2.1.2", + "version": "v2.2.0", "source": { "type": "git", "url": "https://github.com/amphp/dns.git", - "reference": "04c88e67bef804203df934703bd422ea72f46b0e" + "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/04c88e67bef804203df934703bd422ea72f46b0e", - "reference": "04c88e67bef804203df934703bd422ea72f46b0e", + "url": "https://api.github.com/repos/amphp/dns/zipball/758266b0ea7470e2e42cd098493bc6d6c7100cf7", + "reference": "758266b0ea7470e2e42cd098493bc6d6c7100cf7", "shasum": "" }, "require": { @@ -305,7 +305,7 @@ ], "support": { "issues": "https://github.com/amphp/dns/issues", - "source": "https://github.com/amphp/dns/tree/v2.1.2" + "source": "https://github.com/amphp/dns/tree/v2.2.0" }, "funding": [ { @@ -313,7 +313,7 @@ "type": "github" } ], - "time": "2024-04-19T03:49:29+00:00" + "time": "2024-06-02T19:54:12+00:00" }, { "name": "amphp/parallel", @@ -463,16 +463,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.0", + "version": "v1.2.1", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b" + "reference": "66c095673aa5b6e689e63b52d19e577459129ab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/f1c2ce35d27ae86ead018adb803eccca7421dd9b", - "reference": "f1c2ce35d27ae86ead018adb803eccca7421dd9b", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/66c095673aa5b6e689e63b52d19e577459129ab3", + "reference": "66c095673aa5b6e689e63b52d19e577459129ab3", "shasum": "" }, "require": { @@ -518,7 +518,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.0" + "source": "https://github.com/amphp/pipeline/tree/v1.2.1" }, "funding": [ { @@ -526,7 +526,7 @@ "type": "github" } ], - "time": "2024-03-10T14:48:16+00:00" + "time": "2024-07-04T00:56:47+00:00" }, { "name": "amphp/process", @@ -867,16 +867,16 @@ }, { "name": "aws/aws-crt-php", - "version": "v1.2.5", + "version": "v1.2.6", "source": { "type": "git", "url": "https://github.com/awslabs/aws-crt-php.git", - "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b" + "reference": "a63485b65b6b3367039306496d49737cf1995408" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/0ea1f04ec5aa9f049f97e012d1ed63b76834a31b", - "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/a63485b65b6b3367039306496d49737cf1995408", + "reference": "a63485b65b6b3367039306496d49737cf1995408", "shasum": "" }, "require": { @@ -915,22 +915,22 @@ ], "support": { "issues": "https://github.com/awslabs/aws-crt-php/issues", - "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.5" + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.6" }, - "time": "2024-04-19T21:30:56+00:00" + "time": "2024-06-13T17:21:28+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.308.4", + "version": "3.316.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6" + "reference": "888cee2adf890a5b749cc22c0f05051b53619d33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6", - "reference": "c88e9df7e076b6e2c652a1c87d2c3af0a9ac30b6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/888cee2adf890a5b749cc22c0f05051b53619d33", + "reference": "888cee2adf890a5b749cc22c0f05051b53619d33", "shasum": "" }, "require": { @@ -1010,9 +1010,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.308.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.316.1" }, - "time": "2024-05-28T18:05:38+00:00" + "time": "2024-07-09T18:09:27+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1197,72 +1197,6 @@ ], "time": "2023-12-11T17:09:12+00:00" }, - { - "name": "clue/stream-filter", - "version": "v1.7.0", - "source": { - "type": "git", - "url": "https://github.com/clue/stream-filter.git", - "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", - "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", - "shasum": "" - }, - "require": { - "php": ">=5.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "Clue\\StreamFilter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - } - ], - "description": "A simple and modern approach to stream filtering in PHP", - "homepage": "https://github.com/clue/stream-filter", - "keywords": [ - "bucket brigade", - "callback", - "filter", - "php_user_filter", - "stream", - "stream_filter_append", - "stream_filter_register" - ], - "support": { - "issues": "https://github.com/clue/stream-filter/issues", - "source": "https://github.com/clue/stream-filter/tree/v1.7.0" - }, - "funding": [ - { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2023-12-20T15:40:13+00:00" - }, { "name": "danharrin/livewire-rate-limiting", "version": "v1.3.1", @@ -1413,16 +1347,16 @@ }, { "name": "dflydev/dot-access-data", - "version": "v3.0.2", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/dflydev/dflydev-dot-access-data.git", - "reference": "f41715465d65213d644d3141a6a93081be5d3549" + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", - "reference": "f41715465d65213d644d3141a6a93081be5d3549", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", "shasum": "" }, "require": { @@ -1482,9 +1416,9 @@ ], "support": { "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", - "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" }, - "time": "2022-10-27T11:44:00+00:00" + "time": "2024-07-08T12:26:09+00:00" }, { "name": "doctrine/cache", @@ -1581,16 +1515,16 @@ }, { "name": "doctrine/dbal", - "version": "3.8.4", + "version": "3.8.6", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "b05e48a745f722801f55408d0dbd8003b403dbbd" + "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/b05e48a745f722801f55408d0dbd8003b403dbbd", - "reference": "b05e48a745f722801f55408d0dbd8003b403dbbd", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/b7411825cf7efb7e51f9791dea19d86e43b399a1", + "reference": "b7411825cf7efb7e51f9791dea19d86e43b399a1", "shasum": "" }, "require": { @@ -1606,12 +1540,12 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.58", - "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.16", + "phpstan/phpstan": "1.11.5", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "9.6.19", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.9.0", + "squizlabs/php_codesniffer": "3.10.1", "symfony/cache": "^5.4|^6.0|^7.0", "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" @@ -1674,7 +1608,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.8.4" + "source": "https://github.com/doctrine/dbal/tree/3.8.6" }, "funding": [ { @@ -1690,7 +1624,7 @@ "type": "tidelift" } ], - "time": "2024-04-25T07:04:44+00:00" + "time": "2024-06-19T10:38:17+00:00" }, { "name": "doctrine/deprecations", @@ -2733,64 +2667,6 @@ ], "time": "2023-12-03T19:50:20+00:00" }, - { - "name": "http-interop/http-factory-guzzle", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/http-interop/http-factory-guzzle.git", - "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", - "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", - "shasum": "" - }, - "require": { - "guzzlehttp/psr7": "^1.7||^2.0", - "php": ">=7.3", - "psr/http-factory": "^1.0" - }, - "provide": { - "psr/http-factory-implementation": "^1.0" - }, - "require-dev": { - "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^9.5" - }, - "suggest": { - "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Http\\Factory\\Guzzle\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "An HTTP Factory using Guzzle PSR7", - "keywords": [ - "factory", - "http", - "psr-17", - "psr-7" - ], - "support": { - "issues": "https://github.com/http-interop/http-factory-guzzle/issues", - "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" - }, - "time": "2021-07-21T13:50:14+00:00" - }, { "name": "jean85/pretty-package-versions", "version": "2.0.6", @@ -2910,16 +2786,16 @@ }, { "name": "laravel/fortify", - "version": "v1.21.3", + "version": "v1.21.5", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "a725684d17959c4750f3b441ff2e94ecde7793a1" + "reference": "3eaf01ec826c4f653628202640a4450784f78b15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/a725684d17959c4750f3b441ff2e94ecde7793a1", - "reference": "a725684d17959c4750f3b441ff2e94ecde7793a1", + "url": "https://api.github.com/repos/laravel/fortify/zipball/3eaf01ec826c4f653628202640a4450784f78b15", + "reference": "3eaf01ec826c4f653628202640a4450784f78b15", "shasum": "" }, "require": { @@ -2971,20 +2847,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2024-05-08T18:07:38+00:00" + "time": "2024-07-04T14:36:27+00:00" }, { "name": "laravel/framework", - "version": "v10.48.12", + "version": "v11.15.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "590afea38e708022662629fbf5184351fa82cf08" + "reference": "ba85f1c019bed59b3c736c9c4502805efd0ba84b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/590afea38e708022662629fbf5184351fa82cf08", - "reference": "590afea38e708022662629fbf5184351fa82cf08", + "url": "https://api.github.com/repos/laravel/framework/zipball/ba85f1c019bed59b3c736c9c4502805efd0ba84b", + "reference": "ba85f1c019bed59b3c736c9c4502805efd0ba84b", "shasum": "" }, "require": { @@ -3000,44 +2876,44 @@ "ext-openssl": "*", "ext-session": "*", "ext-tokenizer": "*", - "fruitcake/php-cors": "^1.2", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.9", + "laravel/prompts": "^0.1.18", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.67", - "nunomaduro/termwind": "^1.13", - "php": "^8.1", + "nesbot/carbon": "^2.72.2|^3.0", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^6.2", - "symfony/error-handler": "^6.2", - "symfony/finder": "^6.2", - "symfony/http-foundation": "^6.4", - "symfony/http-kernel": "^6.2", - "symfony/mailer": "^6.2", - "symfony/mime": "^6.2", - "symfony/process": "^6.2", - "symfony/routing": "^6.2", - "symfony/uid": "^6.2", - "symfony/var-dumper": "^6.2", + "symfony/console": "^7.0", + "symfony/error-handler": "^7.0", + "symfony/finder": "^7.0", + "symfony/http-foundation": "^7.0", + "symfony/http-kernel": "^7.0", + "symfony/mailer": "^7.0", + "symfony/mime": "^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^7.0", + "symfony/routing": "^7.0", + "symfony/uid": "^7.0", + "symfony/var-dumper": "^7.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5", "vlucas/phpdotenv": "^5.4.1", "voku/portable-ascii": "^2.0" }, "conflict": { - "carbonphp/carbon-doctrine-types": ">=3.0", - "doctrine/dbal": ">=4.0", "mockery/mockery": "1.6.8", - "phpunit/phpunit": ">=11.0.0", "tightenco/collect": "<5.5.33" }, "provide": { "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", "psr/simple-cache-implementation": "1.0|2.0|3.0" }, "replace": { @@ -3073,36 +2949,35 @@ "illuminate/testing": "self.version", "illuminate/translation": "self.version", "illuminate/validation": "self.version", - "illuminate/view": "self.version" + "illuminate/view": "self.version", + "spatie/once": "*" }, "require-dev": { "ably/ably-php": "^1.0", "aws/aws-sdk-php": "^3.235.5", - "doctrine/dbal": "^3.5.1", "ext-gmp": "*", - "fakerphp/faker": "^1.21", - "guzzlehttp/guzzle": "^7.5", + "fakerphp/faker": "^1.23", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-ftp": "^3.0", "league/flysystem-path-prefixing": "^3.3", "league/flysystem-read-only": "^3.3", "league/flysystem-sftp-v3": "^3.0", - "mockery/mockery": "^1.5.1", + "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^8.23.4", - "pda/pheanstalk": "^4.0", - "phpstan/phpstan": "^1.4.7", - "phpunit/phpunit": "^10.0.7", + "orchestra/testbench-core": "^9.1.5", + "pda/pheanstalk": "^5.0", + "phpstan/phpstan": "^1.11.5", + "phpunit/phpunit": "^10.5|^11.0", "predis/predis": "^2.0.2", - "symfony/cache": "^6.2", - "symfony/http-client": "^6.2.4", - "symfony/psr-http-message-bridge": "^2.0" + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.0", + "symfony/http-client": "^7.0", + "symfony/psr-http-message-bridge": "^7.0" }, "suggest": { "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", - "brianium/paratest": "Required to run tests in parallel (^6.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", "ext-apcu": "Required to use the APC cache driver.", "ext-fileinfo": "Required to use the Filesystem class.", "ext-ftp": "Required to use the Flysystem FTP driver.", @@ -3111,34 +2986,34 @@ "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", "ext-pdo": "Required to use all database features.", "ext-posix": "Required to use all features of the queue worker.", - "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", - "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.5).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).", "league/flysystem-read-only": "Required to use read-only disks (^3.3)", "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", - "mockery/mockery": "Required to use mocking (^1.5.1).", + "mockery/mockery": "Required to use mocking (^1.6).", "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", - "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", - "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8|^10.0.7).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).", "predis/predis": "Required to use the predis connector (^2.0.2).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^6.2).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^6.2).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.2).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.2).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.2).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)." + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "10.x-dev" + "dev-master": "11.x-dev" } }, "autoload": { @@ -3178,20 +3053,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-05-28T15:46:19+00:00" + "time": "2024-07-09T15:38:12+00:00" }, { "name": "laravel/horizon", - "version": "v5.24.4", + "version": "v5.25.0", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "8d31ff178bf5493efc2b2629c10612054f31f584" + "reference": "81e62cee5b3feaf169d683b8890e33bf454698ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/8d31ff178bf5493efc2b2629c10612054f31f584", - "reference": "8d31ff178bf5493efc2b2629c10612054f31f584", + "url": "https://api.github.com/repos/laravel/horizon/zipball/81e62cee5b3feaf169d683b8890e33bf454698ab", + "reference": "81e62cee5b3feaf169d683b8890e33bf454698ab", "shasum": "" }, "require": { @@ -3255,22 +3130,22 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.24.4" + "source": "https://github.com/laravel/horizon/tree/v5.25.0" }, - "time": "2024-05-03T13:34:14+00:00" + "time": "2024-07-05T16:46:31+00:00" }, { "name": "laravel/prompts", - "version": "v0.1.23", + "version": "v0.1.24", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "9bc4df7c699b0452c6b815e64a2d84b6d7f99400" + "reference": "409b0b4305273472f3754826e68f4edbd0150149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/9bc4df7c699b0452c6b815e64a2d84b6d7f99400", - "reference": "9bc4df7c699b0452c6b815e64a2d84b6d7f99400", + "url": "https://api.github.com/repos/laravel/prompts/zipball/409b0b4305273472f3754826e68f4edbd0150149", + "reference": "409b0b4305273472f3754826e68f4edbd0150149", "shasum": "" }, "require": { @@ -3313,43 +3188,41 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.23" + "source": "https://github.com/laravel/prompts/tree/v0.1.24" }, - "time": "2024-05-27T13:53:20+00:00" + "time": "2024-06-17T13:58:22+00:00" }, { "name": "laravel/sanctum", - "version": "v3.3.3", + "version": "v4.0.2", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5" + "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/8c104366459739f3ada0e994bcd3e6fd681ce3d5", - "reference": "8c104366459739f3ada0e994bcd3e6fd681ce3d5", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/9cfc0ce80cabad5334efff73ec856339e8ec1ac1", + "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1", "shasum": "" }, "require": { "ext-json": "*", - "illuminate/console": "^9.21|^10.0", - "illuminate/contracts": "^9.21|^10.0", - "illuminate/database": "^9.21|^10.0", - "illuminate/support": "^9.21|^10.0", - "php": "^8.0.2" + "illuminate/console": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/database": "^11.0", + "illuminate/support": "^11.0", + "php": "^8.2", + "symfony/console": "^7.0" }, "require-dev": { - "mockery/mockery": "^1.0", - "orchestra/testbench": "^7.28.2|^8.8.3", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - }, "laravel": { "providers": [ "Laravel\\Sanctum\\SanctumServiceProvider" @@ -3381,7 +3254,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2023-12-19T18:44:48+00:00" + "time": "2024-04-10T19:39:58+00:00" }, { "name": "laravel/serializable-closure", @@ -3445,16 +3318,16 @@ }, { "name": "laravel/socialite", - "version": "v5.14.0", + "version": "v5.15.1", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a" + "reference": "cc02625f0bd1f95dc3688eb041cce0f1e709d029" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/c7b0193a3753a29aff8ce80aa2f511917e6ed68a", - "reference": "c7b0193a3753a29aff8ce80aa2f511917e6ed68a", + "url": "https://api.github.com/repos/laravel/socialite/zipball/cc02625f0bd1f95dc3688eb041cce0f1e709d029", + "reference": "cc02625f0bd1f95dc3688eb041cce0f1e709d029", "shasum": "" }, "require": { @@ -3513,7 +3386,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2024-05-03T20:31:38+00:00" + "time": "2024-06-28T20:09:34+00:00" }, { "name": "laravel/tinker", @@ -4449,16 +4322,16 @@ }, { "name": "livewire/livewire", - "version": "v3.4.9", + "version": "v3.5.2", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0" + "reference": "636725c1f87bc7844dd80277488268db27eec1aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0", - "reference": "c65b3f0798ab2c9338213ede3588c3cdf4e6fcc0", + "url": "https://api.github.com/repos/livewire/livewire/zipball/636725c1f87bc7844dd80277488268db27eec1aa", + "reference": "636725c1f87bc7844dd80277488268db27eec1aa", "shasum": "" }, "require": { @@ -4468,15 +4341,16 @@ "illuminate/validation": "^10.0|^11.0", "league/mime-type-detection": "^1.9", "php": "^8.1", + "symfony/console": "^6.0|^7.0", "symfony/http-kernel": "^6.2|^7.0" }, "require-dev": { "calebporzio/sushi": "^2.1", - "laravel/framework": "^10.0|^11.0", + "laravel/framework": "^10.15.0|^11.0", "laravel/prompts": "^0.1.6", "mockery/mockery": "^1.3.1", - "orchestra/testbench": "8.20.0|^9.0", - "orchestra/testbench-dusk": "8.20.0|^9.0", + "orchestra/testbench": "^8.21.0|^9.0", + "orchestra/testbench-dusk": "^8.24|^9.1", "phpunit/phpunit": "^10.4", "psy/psysh": "^0.11.22|^0.12" }, @@ -4512,7 +4386,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.4.9" + "source": "https://github.com/livewire/livewire/tree/v3.5.2" }, "funding": [ { @@ -4520,7 +4394,69 @@ "type": "github" } ], - "time": "2024-03-14T14:03:32+00:00" + "time": "2024-07-03T17:22:45+00:00" + }, + { + "name": "log1x/laravel-webfonts", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/Log1x/laravel-webfonts.git", + "reference": "0d38122aa7f5501394006a6715f7d97dac223507" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Log1x/laravel-webfonts/zipball/0d38122aa7f5501394006a6715f7d97dac223507", + "reference": "0d38122aa7f5501394006a6715f7d97dac223507", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.8", + "laravel/prompts": "^0.1.15", + "php": ">=8.1" + }, + "require-dev": { + "illuminate/console": "^10.41", + "illuminate/http": "^10.41", + "illuminate/support": "^10.41", + "laravel/pint": "^1.13" + }, + "type": "package", + "extra": { + "laravel": { + "providers": [ + "Log1x\\LaravelWebfonts\\WebfontsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Log1x\\LaravelWebfonts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brandon Nifong", + "email": "brandon@tendency.me", + "homepage": "https://github.com/log1x" + } + ], + "description": "Download, install, and preload over 1500 Google fonts locally in your Laravel project", + "support": { + "issues": "https://github.com/Log1x/laravel-webfonts/issues", + "source": "https://github.com/Log1x/laravel-webfonts/tree/v1.0.1" + }, + "funding": [ + { + "url": "https://github.com/Log1x", + "type": "github" + } + ], + "time": "2024-03-28T11:53:11+00:00" }, { "name": "lorisleiva/laravel-actions", @@ -4671,16 +4607,16 @@ }, { "name": "monolog/monolog", - "version": "3.6.0", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654" + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", - "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f4393b648b78a5408747de94fca38beb5f7e9ef8", + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8", "shasum": "" }, "require": { @@ -4756,7 +4692,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.6.0" + "source": "https://github.com/Seldaek/monolog/tree/3.7.0" }, "funding": [ { @@ -4768,7 +4704,7 @@ "type": "tidelift" } ], - "time": "2024-04-12T21:02:21+00:00" + "time": "2024-06-28T09:40:51+00:00" }, { "name": "mtdowling/jmespath.php", @@ -4838,42 +4774,41 @@ }, { "name": "nesbot/carbon", - "version": "2.72.3", + "version": "3.6.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83" + "reference": "39c8ef752db6865717cc3fba63970c16f057982c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/0c6fd108360c562f6e4fd1dedb8233b423e91c83", - "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/39c8ef752db6865717cc3fba63970c16f057982c", + "reference": "39c8ef752db6865717cc3fba63970c16f057982c", "shasum": "" }, "require": { "carbonphp/carbon-doctrine-types": "*", "ext-json": "*", - "php": "^7.1.8 || ^8.0", + "php": "^8.1", "psr/clock": "^1.0", + "symfony/clock": "^6.3 || ^7.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16", - "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" }, "provide": { "psr/clock-implementation": "1.0" }, "require-dev": { - "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", - "doctrine/orm": "^2.7 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.0", - "kylekatarnls/multi-tester": "^2.0", - "ondrejmirtes/better-reflection": "*", - "phpmd/phpmd": "^2.9", - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.12.99 || ^1.7.14", - "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", - "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", - "squizlabs/php_codesniffer": "^3.4" + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.57.2", + "kylekatarnls/multi-tester": "^2.5.3", + "ondrejmirtes/better-reflection": "^6.25.0.4", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.11.2", + "phpunit/phpunit": "^10.5.20", + "squizlabs/php_codesniffer": "^3.9.0" }, "bin": [ "bin/carbon" @@ -4881,8 +4816,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-3.x": "3.x-dev", - "dev-master": "2.x-dev" + "dev-master": "3.x-dev", + "dev-2.x": "2.x-dev" }, "laravel": { "providers": [ @@ -4941,7 +4876,7 @@ "type": "tidelift" } ], - "time": "2024-01-25T10:35:09+00:00" + "time": "2024-06-20T15:52:59+00:00" }, { "name": "nette/schema", @@ -5093,16 +5028,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.0.2", + "version": "v5.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", + "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", "shasum": "" }, "require": { @@ -5113,7 +5048,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -5145,9 +5080,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" }, - "time": "2024-03-05T20:51:40+00:00" + "time": "2024-07-01T20:03:41+00:00" }, { "name": "nubs/random-name-generator", @@ -5204,33 +5139,32 @@ }, { "name": "nunomaduro/termwind", - "version": "v1.15.1", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc" + "reference": "58c4c58cf23df7f498daeb97092e34f5259feb6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/8ab0b32c8caa4a2e09700ea32925441385e4a5dc", - "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/58c4c58cf23df7f498daeb97092e34f5259feb6a", + "reference": "58c4c58cf23df7f498daeb97092e34f5259feb6a", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^8.0", - "symfony/console": "^5.3.0|^6.0.0" + "php": "^8.2", + "symfony/console": "^7.0.4" }, "require-dev": { - "ergebnis/phpstan-rules": "^1.0.", - "illuminate/console": "^8.0|^9.0", - "illuminate/support": "^8.0|^9.0", - "laravel/pint": "^1.0.0", - "pestphp/pest": "^1.21.0", - "pestphp/pest-plugin-mock": "^1.0", - "phpstan/phpstan": "^1.4.6", - "phpstan/phpstan-strict-rules": "^1.1.0", - "symfony/var-dumper": "^5.2.7|^6.0.0", + "ergebnis/phpstan-rules": "^2.2.0", + "illuminate/console": "^11.0.0", + "laravel/pint": "^1.14.0", + "mockery/mockery": "^1.6.7", + "pestphp/pest": "^2.34.1", + "phpstan/phpstan": "^1.10.59", + "phpstan/phpstan-strict-rules": "^1.5.2", + "symfony/var-dumper": "^7.0.4", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -5239,6 +5173,9 @@ "providers": [ "Termwind\\Laravel\\TermwindServiceProvider" ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -5270,7 +5207,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v1.15.1" + "source": "https://github.com/nunomaduro/termwind/tree/v2.0.1" }, "funding": [ { @@ -5286,7 +5223,7 @@ "type": "github" } ], - "time": "2023-02-08T01:06:31+00:00" + "time": "2024-03-06T16:17:14+00:00" }, { "name": "nyholm/psr7", @@ -5570,385 +5507,132 @@ "time": "2024-04-22T22:05:04+00:00" }, { - "name": "php-http/client-common", - "version": "2.7.1", + "name": "php-di/invoker", + "version": "2.3.4", "source": { "type": "git", - "url": "https://github.com/php-http/client-common.git", - "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612" + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612", - "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/33234b32dafa8eb69202f950a1fc92055ed76a86", + "reference": "33234b32dafa8eb69202f950a1fc92055ed76a86", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0", - "php-http/httplug": "^2.0", - "php-http/message": "^1.6", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.0 || ^2.0", - "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", - "symfony/polyfill-php80": "^1.17" + "php": ">=7.3", + "psr/container": "^1.0|^2.0" }, "require-dev": { - "doctrine/instantiator": "^1.1", - "guzzlehttp/psr7": "^1.4", - "nyholm/psr7": "^1.2", - "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", - "phpspec/prophecy": "^1.10.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" - }, - "suggest": { - "ext-json": "To detect JSON responses with the ContentTypePlugin", - "ext-libxml": "To detect XML responses with the ContentTypePlugin", - "php-http/cache-plugin": "PSR-6 Cache plugin", - "php-http/logger-plugin": "PSR-3 Logger plugin", - "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0" }, "type": "library", "autoload": { "psr-4": { - "Http\\Client\\Common\\": "src/" + "Invoker\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Common HTTP Client implementations and tools for HTTPlug", - "homepage": "http://httplug.io", + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", "keywords": [ - "client", - "common", - "http", - "httplug" + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" ], "support": { - "issues": "https://github.com/php-http/client-common/issues", - "source": "https://github.com/php-http/client-common/tree/2.7.1" + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.4" }, - "time": "2023-11-30T10:31:25+00:00" + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2023-09-08T09:24:21+00:00" }, { - "name": "php-http/discovery", - "version": "1.19.4", + "name": "php-di/php-di", + "version": "7.0.6", "source": { "type": "git", - "url": "https://github.com/php-http/discovery.git", - "reference": "0700efda8d7526335132360167315fdab3aeb599" + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "8097948a89f6ec782839b3e958432f427cac37fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", - "reference": "0700efda8d7526335132360167315fdab3aeb599", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/8097948a89f6ec782839b3e958432f427cac37fd", + "reference": "8097948a89f6ec782839b3e958432f427cac37fd", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0|^2.0", - "php": "^7.1 || ^8.0" - }, - "conflict": { - "nyholm/psr7": "<1.0", - "zendframework/zend-diactoros": "*" + "laravel/serializable-closure": "^1.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" }, "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "*", - "psr/http-factory-implementation": "*", - "psr/http-message-implementation": "*" + "psr/container-implementation": "^1.0" }, "require-dev": { - "composer/composer": "^1.0.2|^2.0", - "graham-campbell/phpspec-skip-example-extension": "^5.0", - "php-http/httplug": "^1.0 || ^2.0", - "php-http/message-factory": "^1.0", - "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", - "sebastian/comparator": "^3.0.5 || ^4.0.8", - "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" - }, - "type": "composer-plugin", - "extra": { - "class": "Http\\Discovery\\Composer\\Plugin", - "plugin-optional": true - }, - "autoload": { - "psr-4": { - "Http\\Discovery\\": "src/" - }, - "exclude-from-classmap": [ - "src/Composer/Plugin.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", - "homepage": "http://php-http.org", - "keywords": [ - "adapter", - "client", - "discovery", - "factory", - "http", - "message", - "psr17", - "psr7" - ], - "support": { - "issues": "https://github.com/php-http/discovery/issues", - "source": "https://github.com/php-http/discovery/tree/1.19.4" - }, - "time": "2024-03-29T13:00:05+00:00" - }, - { - "name": "php-http/httplug", - "version": "2.4.0", - "source": { - "type": "git", - "url": "https://github.com/php-http/httplug.git", - "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", - "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0", - "php-http/promise": "^1.1", - "psr/http-client": "^1.0", - "psr/http-message": "^1.0 || ^2.0" - }, - "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", - "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Http\\Client\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Eric GELOEN", - "email": "geloen.eric@gmail.com" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "HTTPlug, the HTTP client abstraction for PHP", - "homepage": "http://httplug.io", - "keywords": [ - "client", - "http" - ], - "support": { - "issues": "https://github.com/php-http/httplug/issues", - "source": "https://github.com/php-http/httplug/tree/2.4.0" - }, - "time": "2023-04-14T15:10:03+00:00" - }, - { - "name": "php-http/message", - "version": "1.16.1", - "source": { - "type": "git", - "url": "https://github.com/php-http/message.git", - "reference": "5997f3289332c699fa2545c427826272498a2088" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", - "reference": "5997f3289332c699fa2545c427826272498a2088", - "shasum": "" - }, - "require": { - "clue/stream-filter": "^1.5", - "php": "^7.2 || ^8.0", - "psr/http-message": "^1.1 || ^2.0" - }, - "provide": { - "php-http/message-factory-implementation": "1.0" - }, - "require-dev": { - "ergebnis/composer-normalize": "^2.6", - "ext-zlib": "*", - "guzzlehttp/psr7": "^1.0 || ^2.0", - "laminas/laminas-diactoros": "^2.0 || ^3.0", - "php-http/message-factory": "^1.0.2", - "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", - "slim/slim": "^3.0" + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.6" }, "suggest": { - "ext-zlib": "Used with compressor/decompressor streams", - "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", - "laminas/laminas-diactoros": "Used with Diactoros Factories", - "slim/slim": "Used with Slim Framework PSR-7 implementation" + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" }, "type": "library", "autoload": { "files": [ - "src/filters.php" + "src/functions.php" ], "psr-4": { - "Http\\Message\\": "src/" + "DI\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "HTTP Message related tools", - "homepage": "http://php-http.org", + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", "keywords": [ - "http", - "message", - "psr-7" + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" ], "support": { - "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.16.1" + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.0.6" }, - "time": "2024-03-07T13:22:09+00:00" - }, - { - "name": "php-http/message-factory", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/php-http/message-factory.git", - "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", - "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "psr/http-message": "^1.0 || ^2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Http\\Message\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" - } - ], - "description": "Factory interfaces for PSR-7 HTTP Message", - "homepage": "http://php-http.org", - "keywords": [ - "factory", - "http", - "message", - "stream", - "uri" - ], - "support": { - "issues": "https://github.com/php-http/message-factory/issues", - "source": "https://github.com/php-http/message-factory/tree/1.1.0" - }, - "abandoned": "psr/http-factory", - "time": "2023-04-14T14:16:17+00:00" - }, - { - "name": "php-http/promise", - "version": "1.3.1", - "source": { - "type": "git", - "url": "https://github.com/php-http/promise.git", - "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", - "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", - "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Http\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Joel Wurtz", - "email": "joel.wurtz@gmail.com" + "url": "https://github.com/mnapoli", + "type": "github" }, { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com" + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" } ], - "description": "Promise used for asynchronous HTTP requests", - "homepage": "http://httplug.io", - "keywords": [ - "promise" - ], - "support": { - "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.3.1" - }, - "time": "2024-03-15T13:55:21+00:00" + "time": "2023-11-02T10:04:50+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -6138,20 +5822,20 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.37", + "version": "3.0.39", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" + "reference": "211ebc399c6e73c225a018435fe5ae209d1d1485" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", - "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/211ebc399c6e73c225a018435fe5ae209d1d1485", + "reference": "211ebc399c6e73c225a018435fe5ae209d1d1485", "shasum": "" }, "require": { - "paragonie/constant_time_encoding": "^1|^2", + "paragonie/constant_time_encoding": "^1|^2|^3", "paragonie/random_compat": "^1.4|^2.0|^9.99.99", "php": ">=5.6.1" }, @@ -6228,7 +5912,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.37" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.39" }, "funding": [ { @@ -6244,20 +5928,20 @@ "type": "tidelift" } ], - "time": "2024-03-03T02:14:58+00:00" + "time": "2024-06-24T06:27:33+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "1.29.0", + "version": "1.29.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc" + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/536889f2b340489d328f5ffb7b02bb6b183ddedc", - "reference": "536889f2b340489d328f5ffb7b02bb6b183ddedc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fcaefacf2d5c417e928405b71b400d4ce10daaf4", + "reference": "fcaefacf2d5c417e928405b71b400d4ce10daaf4", "shasum": "" }, "require": { @@ -6289,22 +5973,22 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.29.1" }, - "time": "2024-05-06T12:04:23+00:00" + "time": "2024-05-31T08:52:43+00:00" }, { "name": "phpstan/phpstan", - "version": "1.11.2", + "version": "1.11.7", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "0d5d4294a70deb7547db655c47685d680e39cfec" + "reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0d5d4294a70deb7547db655c47685d680e39cfec", - "reference": "0d5d4294a70deb7547db655c47685d680e39cfec", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/52d2bbfdcae7f895915629e4694e9497d0f8e28d", + "reference": "52d2bbfdcae7f895915629e4694e9497d0f8e28d", "shasum": "" }, "require": { @@ -6349,60 +6033,7 @@ "type": "github" } ], - "time": "2024-05-24T13:23:04+00:00" - }, - { - "name": "pimple/pimple", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/silexphp/Pimple.git", - "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", - "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1 || ^2.0" - }, - "require-dev": { - "symfony/phpunit-bridge": "^5.4@dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4.x-dev" - } - }, - "autoload": { - "psr-0": { - "Pimple": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Pimple, a simple Dependency Injection Container", - "homepage": "https://pimple.symfony.com", - "keywords": [ - "container", - "dependency injection" - ], - "support": { - "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" - }, - "time": "2021-10-28T11:13:42+00:00" + "time": "2024-07-06T11:17:41+00:00" }, { "name": "pion/laravel-chunk-upload", @@ -7029,16 +6660,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.3", + "version": "v0.12.4", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73" + "reference": "2fd717afa05341b4f8152547f142cd2f130f6818" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73", - "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/2fd717afa05341b4f8152547f142cd2f130f6818", + "reference": "2fd717afa05341b4f8152547f142cd2f130f6818", "shasum": "" }, "require": { @@ -7102,9 +6733,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.3" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.4" }, - "time": "2024-04-02T15:57:53+00:00" + "time": "2024-06-10T01:18:23+00:00" }, { "name": "purplepixie/phpdns", @@ -7442,16 +7073,16 @@ }, { "name": "rector/rector", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "556509e2dcf527369892b7d411379c4a02f31859" + "reference": "2fa387553db22b6f9bcccf5ff16f2c2c18a52a65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/556509e2dcf527369892b7d411379c4a02f31859", - "reference": "556509e2dcf527369892b7d411379c4a02f31859", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/2fa387553db22b6f9bcccf5ff16f2c2c18a52a65", + "reference": "2fa387553db22b6f9bcccf5ff16f2c2c18a52a65", "shasum": "" }, "require": { @@ -7489,7 +7120,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.1.0" + "source": "https://github.com/rectorphp/rector/tree/1.2.0" }, "funding": [ { @@ -7497,132 +7128,7 @@ "type": "github" } ], - "time": "2024-05-18T09:40:27+00:00" - }, - { - "name": "resend/resend-laravel", - "version": "v0.5.0", - "source": { - "type": "git", - "url": "https://github.com/resend/resend-laravel.git", - "reference": "e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/resend/resend-laravel/zipball/e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d", - "reference": "e598d1e25e49a7aa4c35f653d1d828f69ee4fc1d", - "shasum": "" - }, - "require": { - "illuminate/support": "^9.21|^10.0", - "php": "^8.1", - "resend/resend-php": "^0.7.1", - "symfony/mailer": "^6.2" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.14", - "mockery/mockery": "^1.5", - "orchestra/testbench": "^7.22|^8.0", - "pestphp/pest": "^1.22" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - }, - "laravel": { - "providers": [ - "Resend\\Laravel\\ResendServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Resend\\Laravel\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Resend and contributors", - "homepage": "https://github.com/resendlabs/resend-laravel/contributors" - } - ], - "description": "Resend for Laravel", - "homepage": "https://resend.com/", - "keywords": [ - "api", - "client", - "laravel", - "php", - "resend", - "sdk" - ], - "support": { - "issues": "https://github.com/resend/resend-laravel/issues", - "source": "https://github.com/resend/resend-laravel/tree/v0.5.0" - }, - "time": "2023-07-15T17:56:14+00:00" - }, - { - "name": "resend/resend-php", - "version": "v0.7.2", - "source": { - "type": "git", - "url": "https://github.com/resend/resend-php.git", - "reference": "bef429c2cd43ae1a1d990059c73750d46f249872" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/resend/resend-php/zipball/bef429c2cd43ae1a1d990059c73750d46f249872", - "reference": "bef429c2cd43ae1a1d990059c73750d46f249872", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^7.5", - "php": "^8.1.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.13", - "pestphp/pest": "^2.0", - "pestphp/pest-plugin-mock": "^2.0" - }, - "type": "library", - "autoload": { - "files": [ - "src/Resend.php" - ], - "psr-4": { - "Resend\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Resend and contributors", - "homepage": "https://github.com/resendlabs/resend-php/contributors" - } - ], - "description": "Resend PHP library.", - "homepage": "https://resend.com/", - "keywords": [ - "api", - "client", - "php", - "resend", - "sdk" - ], - "support": { - "issues": "https://github.com/resend/resend-php/issues", - "source": "https://github.com/resend/resend-php/tree/v0.7.2" - }, - "time": "2023-09-08T23:47:23+00:00" + "time": "2024-07-01T14:24:45+00:00" }, { "name": "revolt/event-loop", @@ -7696,112 +7202,42 @@ }, "time": "2023-11-30T05:34:44+00:00" }, - { - "name": "sentry/sdk", - "version": "3.6.0", - "source": { - "type": "git", - "url": "https://github.com/getsentry/sentry-php-sdk.git", - "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/24c235ff2027401cbea099bf88689e1a1f197c7a", - "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a", - "shasum": "" - }, - "require": { - "http-interop/http-factory-guzzle": "^1.0", - "sentry/sentry": "^3.22", - "symfony/http-client": "^4.3|^5.0|^6.0|^7.0" - }, - "type": "metapackage", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sentry", - "email": "accounts@sentry.io" - } - ], - "description": "This is a metapackage shipping sentry/sentry with a recommended HTTP client.", - "homepage": "http://sentry.io", - "keywords": [ - "crash-reporting", - "crash-reports", - "error-handler", - "error-monitoring", - "log", - "logging", - "sentry" - ], - "support": { - "issues": "https://github.com/getsentry/sentry-php-sdk/issues", - "source": "https://github.com/getsentry/sentry-php-sdk/tree/3.6.0" - }, - "funding": [ - { - "url": "https://sentry.io/", - "type": "custom" - }, - { - "url": "https://sentry.io/pricing/", - "type": "custom" - } - ], - "time": "2023-12-04T10:49:33+00:00" - }, { "name": "sentry/sentry", - "version": "3.22.1", + "version": "4.8.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d" + "reference": "3cf5778ff425a23f2d22ed41b423691d36f47163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", - "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/3cf5778ff425a23f2d22ed41b423691d36f47163", + "reference": "3cf5778ff425a23f2d22ed41b423691d36f47163", "shasum": "" }, "require": { + "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", - "guzzlehttp/promises": "^1.5.3|^2.0", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", "jean85/pretty-package-versions": "^1.5|^2.0.4", "php": "^7.2|^8.0", - "php-http/async-client-implementation": "^1.0", - "php-http/client-common": "^1.5|^2.0", - "php-http/discovery": "^1.15", - "php-http/httplug": "^1.1|^2.0", - "php-http/message": "^1.5", - "php-http/message-factory": "^1.1", - "psr/http-factory": "^1.0", - "psr/http-factory-implementation": "^1.0", "psr/log": "^1.0|^2.0|^3.0", - "symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0|^7.0", - "symfony/polyfill-php80": "^1.17" + "symfony/options-resolver": "^4.4.30|^5.0.11|^6.0|^7.0" }, "conflict": { - "php-http/client-common": "1.8.0", "raven/raven": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.19|3.4.*", + "friendsofphp/php-cs-fixer": "^3.4", + "guzzlehttp/promises": "^1.0|^2.0", "guzzlehttp/psr7": "^1.8.4|^2.1.1", - "http-interop/http-factory-guzzle": "^1.0", "monolog/monolog": "^1.6|^2.0|^3.0", - "nikic/php-parser": "^4.10.3", - "php-http/mock-client": "^1.3", "phpbench/phpbench": "^1.0", - "phpstan/extension-installer": "^1.0", "phpstan/phpstan": "^1.3", - "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5.14|^9.4", - "symfony/phpunit-bridge": "^5.2|^6.0", + "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", "vimeo/psalm": "^4.17" }, "suggest": { @@ -7826,7 +7262,7 @@ "email": "accounts@sentry.io" } ], - "description": "A PHP SDK for Sentry (http://sentry.io)", + "description": "PHP SDK for Sentry (http://sentry.io)", "homepage": "http://sentry.io", "keywords": [ "crash-reporting", @@ -7835,11 +7271,13 @@ "error-monitoring", "log", "logging", - "sentry" + "profiling", + "sentry", + "tracing" ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/3.22.1" + "source": "https://github.com/getsentry/sentry-php/tree/4.8.0" }, "funding": [ { @@ -7851,47 +7289,42 @@ "type": "custom" } ], - "time": "2023-11-13T11:47:28+00:00" + "time": "2024-06-05T13:18:43+00:00" }, { "name": "sentry/sentry-laravel", - "version": "3.8.2", + "version": "4.6.1", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-laravel.git", - "reference": "1293e5732f8405e12f000cdf5dee78c927a18de0" + "reference": "7f5fd9f362e440c4c0c492f386b93095321f9101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/1293e5732f8405e12f000cdf5dee78c927a18de0", - "reference": "1293e5732f8405e12f000cdf5dee78c927a18de0", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/7f5fd9f362e440c4c0c492f386b93095321f9101", + "reference": "7f5fd9f362e440c4c0c492f386b93095321f9101", "shasum": "" }, "require": { - "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", "nyholm/psr7": "^1.0", "php": "^7.2 | ^8.0", - "sentry/sdk": "^3.4", - "sentry/sentry": "^3.20.1", - "symfony/psr-http-message-bridge": "^1.0 | ^2.0" + "sentry/sentry": "^4.7", + "symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.11", - "laravel/folio": "^1.0", - "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "guzzlehttp/guzzle": "^7.2", + "laravel/folio": "^1.1", + "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0", + "livewire/livewire": "^2.0 | ^3.0", "mockery/mockery": "^1.3", - "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0", + "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^8.4 | ^9.3" + "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "3.x-dev", - "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev", - "dev-0.x": "0.x-dev" - }, "laravel": { "providers": [ "Sentry\\Laravel\\ServiceProvider", @@ -7927,11 +7360,13 @@ "laravel", "log", "logging", - "sentry" + "profiling", + "sentry", + "tracing" ], "support": { "issues": "https://github.com/getsentry/sentry-laravel/issues", - "source": "https://github.com/getsentry/sentry-laravel/tree/3.8.2" + "source": "https://github.com/getsentry/sentry-laravel/tree/4.6.1" }, "funding": [ { @@ -7943,7 +7378,7 @@ "type": "custom" } ], - "time": "2023-10-12T14:38:46+00:00" + "time": "2024-06-18T15:06:09+00:00" }, { "name": "socialiteproviders/manager", @@ -8371,16 +7806,16 @@ }, { "name": "spatie/laravel-ray", - "version": "1.36.2", + "version": "1.37.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ray.git", - "reference": "1852faa96e5aa6778ea3401ec3176eee77268718" + "reference": "f57b294a3815be37effa9d13f54f2fbe5a2fff37" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/1852faa96e5aa6778ea3401ec3176eee77268718", - "reference": "1852faa96e5aa6778ea3401ec3176eee77268718", + "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/f57b294a3815be37effa9d13f54f2fbe5a2fff37", + "reference": "f57b294a3815be37effa9d13f54f2fbe5a2fff37", "shasum": "" }, "require": { @@ -8394,7 +7829,7 @@ "spatie/backtrace": "^1.0", "spatie/ray": "^1.41.1", "symfony/stopwatch": "4.2|^5.1|^6.0|^7.0", - "zbateson/mail-mime-parser": "^1.3.1|^2.0" + "zbateson/mail-mime-parser": "^1.3.1|^2.0|^3.0" }, "require-dev": { "guzzlehttp/guzzle": "^7.3", @@ -8442,7 +7877,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-ray/issues", - "source": "https://github.com/spatie/laravel-ray/tree/1.36.2" + "source": "https://github.com/spatie/laravel-ray/tree/1.37.0" }, "funding": [ { @@ -8454,7 +7889,7 @@ "type": "other" } ], - "time": "2024-05-02T08:26:02+00:00" + "time": "2024-07-03T08:48:44+00:00" }, { "name": "spatie/laravel-schemaless-attributes", @@ -8869,48 +8304,121 @@ "time": "2023-10-16T18:04:12+00:00" }, { - "name": "symfony/console", - "version": "v6.4.7", + "name": "symfony/clock", + "version": "v7.1.1", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "a170e64ae10d00ba89e2acbb590dc2e54da8ad8f" + "url": "https://github.com/symfony/clock.git", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a170e64ae10d00ba89e2acbb590dc2e54da8ad8f", - "reference": "a170e64ae10d00ba89e2acbb590dc2e54da8ad8f", + "url": "https://api.github.com/repos/symfony/clock/zipball/3dfc8b084853586de51dd1441c6242c76a28cbe7", + "reference": "3dfc8b084853586de51dd1441c6242c76a28cbe7", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, + { + "name": "symfony/console", + "version": "v7.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "0aa29ca177f432ab68533432db0de059f39c92ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/0aa29ca177f432ab68533432db0de059f39c92ae", + "reference": "0aa29ca177f432ab68533432db0de059f39c92ae", + "shasum": "" + }, + "require": { + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^6.4|^7.0" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -8944,7 +8452,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.7" + "source": "https://github.com/symfony/console/tree/v7.1.2" }, "funding": [ { @@ -8960,20 +8468,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-06-28T10:03:55+00:00" }, { "name": "symfony/css-selector", - "version": "v7.0.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc" + "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc", - "reference": "b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/1c7cee86c6f812896af54434f8ce29c8d94f9ff4", + "reference": "1c7cee86c6f812896af54434f8ce29c8d94f9ff4", "shasum": "" }, "require": { @@ -9009,7 +8517,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.0.7" + "source": "https://github.com/symfony/css-selector/tree/v7.1.1" }, "funding": [ { @@ -9025,7 +8533,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/deprecation-contracts", @@ -9096,22 +8604,22 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.7", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "667a072466c6a53827ed7b119af93806b884cbb3" + "reference": "2412d3dddb5c9ea51a39cfbff1c565fc9844ca32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/667a072466c6a53827ed7b119af93806b884cbb3", - "reference": "667a072466c6a53827ed7b119af93806b884cbb3", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/2412d3dddb5c9ea51a39cfbff1c565fc9844ca32", + "reference": "2412d3dddb5c9ea51a39cfbff1c565fc9844ca32", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/var-dumper": "^6.4|^7.0" }, "conflict": { "symfony/deprecation-contracts": "<2.5", @@ -9120,7 +8628,7 @@ "require-dev": { "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-kernel": "^6.4|^7.0", - "symfony/serializer": "^5.4|^6.0|^7.0" + "symfony/serializer": "^6.4|^7.0" }, "bin": [ "Resources/bin/patch-type-declarations" @@ -9151,7 +8659,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.7" + "source": "https://github.com/symfony/error-handler/tree/v7.1.2" }, "funding": [ { @@ -9167,20 +8675,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-06-25T19:55:06+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.0.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9" + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/db2a7fab994d67d92356bb39c367db115d9d30f9", - "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", + "reference": "9fa7f7a21beb22a39a8f3f28618b29e50d7a55a7", "shasum": "" }, "require": { @@ -9231,7 +8739,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.7" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.1" }, "funding": [ { @@ -9247,7 +8755,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -9327,23 +8835,23 @@ }, { "name": "symfony/finder", - "version": "v6.4.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "511c48990be17358c23bf45c5d71ab85d40fb764" + "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/511c48990be17358c23bf45c5d71ab85d40fb764", - "reference": "511c48990be17358c23bf45c5d71ab85d40fb764", + "url": "https://api.github.com/repos/symfony/finder/zipball/fbb0ba67688b780efbc886c1a0a0948dcf7205d6", + "reference": "fbb0ba67688b780efbc886c1a0a0948dcf7205d6", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.0|^7.0" + "symfony/filesystem": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9371,7 +8879,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v6.4.7" + "source": "https://github.com/symfony/finder/tree/v7.1.1" }, "funding": [ { @@ -9387,211 +8895,40 @@ "type": "tidelift" } ], - "time": "2024-04-23T10:36:43+00:00" - }, - { - "name": "symfony/http-client", - "version": "v6.4.7", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client.git", - "reference": "3683d8107cf1efdd24795cc5f7482be1eded34ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/3683d8107cf1efdd24795cc5f7482be1eded34ac", - "reference": "3683d8107cf1efdd24795cc5f7482be1eded34ac", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/log": "^1|^2|^3", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/http-client-contracts": "^3.4.1", - "symfony/service-contracts": "^2.5|^3" - }, - "conflict": { - "php-http/discovery": "<1.15", - "symfony/http-foundation": "<6.3" - }, - "provide": { - "php-http/async-client-implementation": "*", - "php-http/client-implementation": "*", - "psr/http-client-implementation": "1.0", - "symfony/http-client-implementation": "3.0" - }, - "require-dev": { - "amphp/amp": "^2.5", - "amphp/http-client": "^4.2.1", - "amphp/http-tunnel": "^1.0", - "amphp/socket": "^1.1", - "guzzlehttp/promises": "^1.4|^2.0", - "nyholm/psr7": "^1.0", - "php-http/httplug": "^1.0|^2.0", - "psr/http-client": "^1.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", - "homepage": "https://symfony.com", - "keywords": [ - "http" - ], - "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.7" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:22:46+00:00" - }, - { - "name": "symfony/http-client-contracts", - "version": "v3.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "20414d96f391677bf80078aa55baece78b82647d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", - "reference": "20414d96f391677bf80078aa55baece78b82647d", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - }, - "exclude-from-classmap": [ - "/Test/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to HTTP clients", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-04-18T09:32:20+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/http-foundation", - "version": "v6.4.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "b4db6b833035477cb70e18d0ae33cb7c2b521759" + "reference": "74d171d5b6a1d9e4bfee09a41937c17a7536acfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b4db6b833035477cb70e18d0ae33cb7c2b521759", - "reference": "b4db6b833035477cb70e18d0ae33cb7c2b521759", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/74d171d5b6a1d9e4bfee09a41937c17a7536acfa", + "reference": "74d171d5b6a1d9e4bfee09a41937c17a7536acfa", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php83": "^1.27" }, "conflict": { - "symfony/cache": "<6.3" + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4" }, "require-dev": { - "doctrine/dbal": "^2.13.1|^3|^4", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.3|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", - "symfony/mime": "^5.4|^6.0|^7.0", - "symfony/rate-limiter": "^5.4|^6.0|^7.0" + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9619,7 +8956,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.7" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.1" }, "funding": [ { @@ -9635,77 +8972,77 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/http-kernel", - "version": "v6.4.7", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "b7b5e6cdef670a0c82d015a966ffc7e855861a98" + "reference": "ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b7b5e6cdef670a0c82d015a966ffc7e855861a98", - "reference": "b7b5e6cdef670a0c82d015a966ffc7e855861a98", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6", + "reference": "ae3fa717db4d41a55d14c2bd92399e37cf5bc0f6", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "psr/log": "^1|^2|^3", "symfony/deprecation-contracts": "^2.5|^3", "symfony/error-handler": "^6.4|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.4", - "symfony/config": "<6.1", - "symfony/console": "<5.4", + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", "symfony/dependency-injection": "<6.4", - "symfony/doctrine-bridge": "<5.4", - "symfony/form": "<5.4", - "symfony/http-client": "<5.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/mailer": "<5.4", - "symfony/messenger": "<5.4", - "symfony/translation": "<5.4", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", "symfony/translation-contracts": "<2.5", - "symfony/twig-bridge": "<5.4", + "symfony/twig-bridge": "<6.4", "symfony/validator": "<6.4", - "symfony/var-dumper": "<6.3", - "twig/twig": "<2.13" + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.0.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0|^7.0", - "symfony/clock": "^6.2|^7.0", - "symfony/config": "^6.1|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", "symfony/dependency-injection": "^6.4|^7.0", - "symfony/dom-crawler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/property-access": "^5.4.5|^6.0.5|^7.0", - "symfony/routing": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.4.4|^7.0.4", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3", - "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/uid": "^6.4|^7.0", "symfony/validator": "^6.4|^7.0", - "symfony/var-dumper": "^5.4|^6.4|^7.0", - "symfony/var-exporter": "^6.2|^7.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.0.4" }, "type": "library", "autoload": { @@ -9733,7 +9070,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.7" + "source": "https://github.com/symfony/http-kernel/tree/v7.1.2" }, "funding": [ { @@ -9749,43 +9086,43 @@ "type": "tidelift" } ], - "time": "2024-04-29T11:24:44+00:00" + "time": "2024-06-28T13:13:31+00:00" }, { "name": "symfony/mailer", - "version": "v6.4.7", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "2c446d4e446995bed983c0b5bb9ff837e8de7dbd" + "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/2c446d4e446995bed983c0b5bb9ff837e8de7dbd", - "reference": "2c446d4e446995bed983c0b5bb9ff837e8de7dbd", + "url": "https://api.github.com/repos/symfony/mailer/zipball/8fcff0af9043c8f8a8e229437cea363e282f9aee", + "reference": "8fcff0af9043c8f8a8e229437cea363e282f9aee", "shasum": "" }, "require": { "egulias/email-validator": "^2.1.10|^3|^4", - "php": ">=8.1", + "php": ">=8.2", "psr/event-dispatcher": "^1", "psr/log": "^1|^2|^3", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/mime": "^6.2|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3" }, "conflict": { "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4", - "symfony/messenger": "<6.2", - "symfony/mime": "<6.2", - "symfony/twig-bridge": "<6.2.1" + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/messenger": "^6.2|^7.0", - "symfony/twig-bridge": "^6.2|^7.0" + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -9813,7 +9150,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.4.7" + "source": "https://github.com/symfony/mailer/tree/v7.1.2" }, "funding": [ { @@ -9829,25 +9166,24 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-06-28T08:00:31+00:00" }, { "name": "symfony/mime", - "version": "v6.4.7", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "decadcf3865918ecfcbfa90968553994ce935a5e" + "reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/decadcf3865918ecfcbfa90968553994ce935a5e", - "reference": "decadcf3865918ecfcbfa90968553994ce935a5e", + "url": "https://api.github.com/repos/symfony/mime/zipball/26a00b85477e69a4bab63b66c5dce64f18b0cbfc", + "reference": "26a00b85477e69a4bab63b66c5dce64f18b0cbfc", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "^1.0" }, @@ -9855,18 +9191,18 @@ "egulias/email-validator": "~3.0.0", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<5.4", - "symfony/serializer": "<6.3.2" + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.4|^7.0", - "symfony/property-access": "^5.4|^6.0|^7.0", - "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.3.2|^7.0" + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" }, "type": "library", "autoload": { @@ -9898,7 +9234,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.7" + "source": "https://github.com/symfony/mime/tree/v7.1.2" }, "funding": [ { @@ -9914,20 +9250,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-06-28T10:03:55+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.0.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa" + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/23cc173858776ad451e31f053b1c9f47840b2cfa", - "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55", + "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55", "shasum": "" }, "require": { @@ -9965,7 +9301,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.0.7" + "source": "https://github.com/symfony/options-resolver/tree/v7.1.1" }, "funding": [ { @@ -9981,20 +9317,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -10044,7 +9380,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -10060,20 +9396,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f" + "reference": "c027e6a3c6aee334663ec21f5852e89738abc805" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", - "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/c027e6a3c6aee334663ec21f5852e89738abc805", + "reference": "c027e6a3c6aee334663ec21f5852e89738abc805", "shasum": "" }, "require": { @@ -10124,7 +9460,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.30.0" }, "funding": [ { @@ -10140,20 +9476,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -10202,7 +9538,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -10218,20 +9554,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" + "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", - "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", + "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", "shasum": "" }, "require": { @@ -10286,7 +9622,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0" }, "funding": [ { @@ -10302,20 +9638,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -10367,7 +9703,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -10383,20 +9719,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -10447,7 +9783,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -10463,20 +9799,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" + "reference": "10112722600777e02d2745716b70c5db4ca70442" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", - "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442", + "reference": "10112722600777e02d2745716b70c5db4ca70442", "shasum": "" }, "require": { @@ -10520,7 +9856,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0" }, "funding": [ { @@ -10536,20 +9872,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -10600,7 +9936,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -10616,25 +9952,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff" + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff", - "reference": "86fcae159633351e5fd145d1c47de6c528f8caff", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", + "reference": "dbdcdf1a4dcc2743591f1079d0c35ab1e2dcbbc9", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-php80": "^1.14" + "php": ">=7.1" }, "type": "library", "extra": { @@ -10677,7 +10012,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.30.0" }, "funding": [ { @@ -10693,20 +10028,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:35:24+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", - "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853" + "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853", - "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/2ba1f33797470debcda07fe9dce20a0003df18e9", + "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9", "shasum": "" }, "require": { @@ -10756,7 +10091,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.30.0" }, "funding": [ { @@ -10772,24 +10107,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/process", - "version": "v6.4.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cdb1c81c145fd5aa9b0038bab694035020943381" + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cdb1c81c145fd5aa9b0038bab694035020943381", - "reference": "cdb1c81c145fd5aa9b0038bab694035020943381", + "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028", + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028", "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -10817,7 +10152,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v6.4.7" + "source": "https://github.com/symfony/process/tree/v7.1.1" }, "funding": [ { @@ -10833,47 +10168,42 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v2.3.1", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" + "reference": "9a5dbb606da711f5d40a7596ad577856f9402140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", - "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/9a5dbb606da711f5d40a7596ad577856f9402140", + "reference": "9a5dbb606da711f5d40a7596ad577856f9402140", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/http-message": "^1.0 || ^2.0", - "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/http-foundation": "^5.4 || ^6.0" + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" }, "require-dev": { "nyholm/psr7": "^1.1", - "psr/log": "^1.1 || ^2 || ^3", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/config": "^5.4 || ^6.0", - "symfony/event-dispatcher": "^5.4 || ^6.0", - "symfony/framework-bundle": "^5.4 || ^6.0", - "symfony/http-kernel": "^5.4 || ^6.0", - "symfony/phpunit-bridge": "^6.2" - }, - "suggest": { - "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" }, "type": "symfony-bridge", - "extra": { - "branch-alias": { - "dev-main": "2.3-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Bridge\\PsrHttpMessage\\": "" @@ -10893,11 +10223,11 @@ }, { "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" + "homepage": "https://symfony.com/contributors" } ], "description": "PSR HTTP message bridge", - "homepage": "http://symfony.com", + "homepage": "https://symfony.com", "keywords": [ "http", "http-message", @@ -10905,8 +10235,7 @@ "psr-7" ], "support": { - "issues": "https://github.com/symfony/psr-http-message-bridge/issues", - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1" + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.1.1" }, "funding": [ { @@ -10922,40 +10251,38 @@ "type": "tidelift" } ], - "time": "2023-07-26T11:53:26+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/routing", - "version": "v6.4.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "276e06398f71fa2a973264d94f28150f93cfb907" + "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/276e06398f71fa2a973264d94f28150f93cfb907", - "reference": "276e06398f71fa2a973264d94f28150f93cfb907", + "url": "https://api.github.com/repos/symfony/routing/zipball/60c31bab5c45af7f13091b87deb708830f3c96c0", + "reference": "60c31bab5c45af7f13091b87deb708830f3c96c0", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { - "doctrine/annotations": "<1.12", - "symfony/config": "<6.2", - "symfony/dependency-injection": "<5.4", - "symfony/yaml": "<5.4" + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" }, "require-dev": { - "doctrine/annotations": "^1.12|^2", "psr/log": "^1|^2|^3", - "symfony/config": "^6.2|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -10989,7 +10316,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v6.4.7" + "source": "https://github.com/symfony/routing/tree/v7.1.1" }, "funding": [ { @@ -11005,7 +10332,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/service-contracts", @@ -11092,16 +10419,16 @@ }, { "name": "symfony/stopwatch", - "version": "v7.0.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84" + "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/41a7a24aa1dc82adf46a06bc292d1923acfe6b84", - "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", + "reference": "5b75bb1ac2ba1b9d05c47fc4b3046a625377d23d", "shasum": "" }, "require": { @@ -11134,7 +10461,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.0.7" + "source": "https://github.com/symfony/stopwatch/tree/v7.1.1" }, "funding": [ { @@ -11150,20 +10477,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/string", - "version": "v7.0.7", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63" + "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/e405b5424dc2528e02e31ba26b83a79fd4eb8f63", - "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63", + "url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8", + "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8", "shasum": "" }, "require": { @@ -11177,6 +10504,7 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { + "symfony/emoji": "^7.1", "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", @@ -11220,7 +10548,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.0.7" + "source": "https://github.com/symfony/string/tree/v7.1.2" }, "funding": [ { @@ -11236,37 +10564,36 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:29:19+00:00" + "time": "2024-06-28T09:27:18+00:00" }, { "name": "symfony/translation", - "version": "v6.4.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "7495687c58bfd88b7883823747b0656d90679123" + "reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/7495687c58bfd88b7883823747b0656d90679123", - "reference": "7495687c58bfd88b7883823747b0656d90679123", + "url": "https://api.github.com/repos/symfony/translation/zipball/cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3", + "reference": "cf5ae136e124fc7681b34ce9fac9d5b9ae8ceee3", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0", "symfony/translation-contracts": "^2.5|^3.0" }, "conflict": { - "symfony/config": "<5.4", - "symfony/console": "<5.4", - "symfony/dependency-injection": "<5.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/http-kernel": "<5.4", + "symfony/http-kernel": "<6.4", "symfony/service-contracts": "<2.5", - "symfony/twig-bundle": "<5.4", - "symfony/yaml": "<5.4" + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -11274,17 +10601,17 @@ "require-dev": { "nikic/php-parser": "^4.18|^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/routing": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0|^7.0" + "symfony/yaml": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -11315,7 +10642,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.7" + "source": "https://github.com/symfony/translation/tree/v7.1.1" }, "funding": [ { @@ -11331,7 +10658,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/translation-contracts", @@ -11413,24 +10740,24 @@ }, { "name": "symfony/uid", - "version": "v6.4.7", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "a66efcb71d8bc3a207d9d78e0bd67f3321510355" + "reference": "bb59febeecc81528ff672fad5dab7f06db8c8277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/a66efcb71d8bc3a207d9d78e0bd67f3321510355", - "reference": "a66efcb71d8bc3a207d9d78e0bd67f3321510355", + "url": "https://api.github.com/repos/symfony/uid/zipball/bb59febeecc81528ff672fad5dab7f06db8c8277", + "reference": "bb59febeecc81528ff672fad5dab7f06db8c8277", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-uuid": "^1.15" }, "require-dev": { - "symfony/console": "^5.4|^6.0|^7.0" + "symfony/console": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -11467,7 +10794,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v6.4.7" + "source": "https://github.com/symfony/uid/tree/v7.1.1" }, "funding": [ { @@ -11483,38 +10810,36 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/var-dumper", - "version": "v6.4.7", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "7a9cd977cd1c5fed3694bee52990866432af07d7" + "reference": "5857c57c6b4b86524c08cf4f4bc95327270a816d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7a9cd977cd1c5fed3694bee52990866432af07d7", - "reference": "7a9cd977cd1c5fed3694bee52990866432af07d7", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5857c57c6b4b86524c08cf4f4bc95327270a816d", + "reference": "5857c57c6b4b86524c08cf4f4bc95327270a816d", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/console": "<5.4" + "symfony/console": "<6.4" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^6.3|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/uid": "^5.4|^6.0|^7.0", - "twig/twig": "^2.13|^3.0.4" + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.0.4" }, "bin": [ "Resources/bin/var-dump-server" @@ -11552,7 +10877,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.7" + "source": "https://github.com/symfony/var-dumper/tree/v7.1.2" }, "funding": [ { @@ -11568,20 +10893,20 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-06-28T08:00:31+00:00" }, { "name": "symfony/yaml", - "version": "v6.4.7", + "version": "v6.4.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0" + "reference": "52903de178d542850f6f341ba92995d3d63e60c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0", - "reference": "53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/52903de178d542850f6f341ba92995d3d63e60c9", + "reference": "52903de178d542850f6f341ba92995d3d63e60c9", "shasum": "" }, "require": { @@ -11624,7 +10949,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.7" + "source": "https://github.com/symfony/yaml/tree/v6.4.8" }, "funding": [ { @@ -11640,7 +10965,7 @@ "type": "tidelift" } ], - "time": "2024-04-28T10:28:08+00:00" + "time": "2024-05-31T14:49:08+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -12082,30 +11407,31 @@ }, { "name": "zbateson/mail-mime-parser", - "version": "2.4.1", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/zbateson/mail-mime-parser.git", - "reference": "ff49e02f6489b38f7cc3d1bd3971adc0f872569c" + "reference": "6ade63b0a43047935791d7977e22717a68cc388b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/ff49e02f6489b38f7cc3d1bd3971adc0f872569c", - "reference": "ff49e02f6489b38f7cc3d1bd3971adc0f872569c", + "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/6ade63b0a43047935791d7977e22717a68cc388b", + "reference": "6ade63b0a43047935791d7977e22717a68cc388b", "shasum": "" }, "require": { - "guzzlehttp/psr7": "^1.7.0|^2.0", - "php": ">=7.1", - "pimple/pimple": "^3.0", - "zbateson/mb-wrapper": "^1.0.1", - "zbateson/stream-decorators": "^1.0.6" + "guzzlehttp/psr7": "^2.5", + "php": ">=8.0", + "php-di/php-di": "^6.0|^7.0", + "psr/log": "^1|^2|^3", + "zbateson/mb-wrapper": "^2.0", + "zbateson/stream-decorators": "^2.1" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", - "mikey179/vfsstream": "^1.6.0", + "monolog/monolog": "^2|^3", "phpstan/phpstan": "*", - "phpunit/phpunit": "<10" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-iconv": "For best support/performance", @@ -12153,24 +11479,24 @@ "type": "github" } ], - "time": "2024-04-28T00:58:54+00:00" + "time": "2024-04-29T21:53:01+00:00" }, { "name": "zbateson/mb-wrapper", - "version": "1.2.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/zbateson/mb-wrapper.git", - "reference": "09a8b77eb94af3823a9a6623dcc94f8d988da67f" + "reference": "9e4373a153585d12b6c621ac4a6bb143264d4619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/09a8b77eb94af3823a9a6623dcc94f8d988da67f", - "reference": "09a8b77eb94af3823a9a6623dcc94f8d988da67f", + "url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/9e4373a153585d12b6c621ac4a6bb143264d4619", + "reference": "9e4373a153585d12b6c621ac4a6bb143264d4619", "shasum": "" }, "require": { - "php": ">=7.1", + "php": ">=8.0", "symfony/polyfill-iconv": "^1.9", "symfony/polyfill-mbstring": "^1.9" }, @@ -12214,7 +11540,7 @@ ], "support": { "issues": "https://github.com/zbateson/mb-wrapper/issues", - "source": "https://github.com/zbateson/mb-wrapper/tree/1.2.1" + "source": "https://github.com/zbateson/mb-wrapper/tree/2.0.0" }, "funding": [ { @@ -12222,31 +11548,31 @@ "type": "github" } ], - "time": "2024-03-18T04:31:04+00:00" + "time": "2024-03-20T01:38:07+00:00" }, { "name": "zbateson/stream-decorators", - "version": "1.2.1", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/zbateson/stream-decorators.git", - "reference": "783b034024fda8eafa19675fb2552f8654d3a3e9" + "reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/783b034024fda8eafa19675fb2552f8654d3a3e9", - "reference": "783b034024fda8eafa19675fb2552f8654d3a3e9", + "url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/32a2a62fb0f26313395c996ebd658d33c3f9c4e5", + "reference": "32a2a62fb0f26313395c996ebd658d33c3f9c4e5", "shasum": "" }, "require": { - "guzzlehttp/psr7": "^1.9 | ^2.0", - "php": ">=7.2", - "zbateson/mb-wrapper": "^1.0.0" + "guzzlehttp/psr7": "^2.5", + "php": ">=8.0", + "zbateson/mb-wrapper": "^2.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", "phpstan/phpstan": "*", - "phpunit/phpunit": "<10.0" + "phpunit/phpunit": "^9.6|^10.0" }, "type": "library", "autoload": { @@ -12277,7 +11603,7 @@ ], "support": { "issues": "https://github.com/zbateson/stream-decorators/issues", - "source": "https://github.com/zbateson/stream-decorators/tree/1.2.1" + "source": "https://github.com/zbateson/stream-decorators/tree/2.1.1" }, "funding": [ { @@ -12285,7 +11611,88 @@ "type": "github" } ], - "time": "2023-05-30T22:51:52+00:00" + "time": "2024-04-29T21:42:39+00:00" + }, + { + "name": "zircote/swagger-php", + "version": "4.10.3", + "source": { + "type": "git", + "url": "https://github.com/zircote/swagger-php.git", + "reference": "ad3f913d39b2a4dfb6e59ee4babb35a6b4a2b998" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zircote/swagger-php/zipball/ad3f913d39b2a4dfb6e59ee4babb35a6b4a2b998", + "reference": "ad3f913d39b2a4dfb6e59ee4babb35a6b4a2b998", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.2", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "symfony/deprecation-contracts": "^2 || ^3", + "symfony/finder": ">=2.2", + "symfony/yaml": ">=3.3" + }, + "require-dev": { + "composer/package-versions-deprecated": "^1.11", + "doctrine/annotations": "^1.7 || ^2.0", + "friendsofphp/php-cs-fixer": "^2.17 || ^3.47.1", + "phpstan/phpstan": "^1.6", + "phpunit/phpunit": ">=8", + "vimeo/psalm": "^4.23" + }, + "suggest": { + "doctrine/annotations": "^1.7 || ^2.0" + }, + "bin": [ + "bin/openapi" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, + "autoload": { + "psr-4": { + "OpenApi\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Robert Allen", + "email": "zircote@gmail.com" + }, + { + "name": "Bob Fanger", + "email": "bfanger@gmail.com", + "homepage": "https://bfanger.nl" + }, + { + "name": "Martin Rademacher", + "email": "mano@radebatz.net", + "homepage": "https://radebatz.net" + } + ], + "description": "swagger-php - Generate interactive documentation for your RESTful API using phpdoc annotations", + "homepage": "https://github.com/zircote/swagger-php/", + "keywords": [ + "api", + "json", + "rest", + "service discovery" + ], + "support": { + "issues": "https://github.com/zircote/swagger-php/issues", + "source": "https://github.com/zircote/swagger-php/tree/4.10.3" + }, + "time": "2024-07-04T07:53:11+00:00" } ], "packages-dev": [ @@ -12631,47 +12038,43 @@ }, { "name": "laravel/dusk", - "version": "v7.13.0", + "version": "v8.2.1", "source": { "type": "git", "url": "https://github.com/laravel/dusk.git", - "reference": "dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6" + "reference": "f2c0957aa4fbb4a78394e77b8caf969903f28050" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6", - "reference": "dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6", + "url": "https://api.github.com/repos/laravel/dusk/zipball/f2c0957aa4fbb4a78394e77b8caf969903f28050", + "reference": "f2c0957aa4fbb4a78394e77b8caf969903f28050", "shasum": "" }, "require": { "ext-json": "*", "ext-zip": "*", - "guzzlehttp/guzzle": "^7.2", - "illuminate/console": "^9.0|^10.0", - "illuminate/support": "^9.0|^10.0", - "nesbot/carbon": "^2.0", - "php": "^8.0", + "guzzlehttp/guzzle": "^7.5", + "illuminate/console": "^10.0|^11.0", + "illuminate/support": "^10.0|^11.0", + "php": "^8.1", "php-webdriver/webdriver": "^1.9.0", - "symfony/console": "^6.0", - "symfony/finder": "^6.0", - "symfony/process": "^6.0", + "symfony/console": "^6.2|^7.0", + "symfony/finder": "^6.2|^7.0", + "symfony/process": "^6.2|^7.0", "vlucas/phpdotenv": "^5.2" }, "require-dev": { - "mockery/mockery": "^1.4.2", - "orchestra/testbench": "^7.33|^8.13", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^8.19|^9.0", "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.5.10|^10.0.1", - "psy/psysh": "^0.11.12" + "phpunit/phpunit": "^10.1|^11.0", + "psy/psysh": "^0.11.12|^0.12" }, "suggest": { "ext-pcntl": "Used to gracefully terminate Dusk when tests are running." }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "7.x-dev" - }, "laravel": { "providers": [ "Laravel\\Dusk\\DuskServiceProvider" @@ -12701,22 +12104,22 @@ ], "support": { "issues": "https://github.com/laravel/dusk/issues", - "source": "https://github.com/laravel/dusk/tree/v7.13.0" + "source": "https://github.com/laravel/dusk/tree/v8.2.1" }, - "time": "2024-02-23T22:29:53+00:00" + "time": "2024-07-08T06:42:12+00:00" }, { "name": "laravel/pint", - "version": "v1.16.0", + "version": "v1.16.2", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98" + "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98", - "reference": "1b3a3dc5bc6a81ff52828ba7277621f1d49d6d98", + "url": "https://api.github.com/repos/laravel/pint/zipball/51f1ba679a6afe0315621ad143d788bd7ded0eca", + "reference": "51f1ba679a6afe0315621ad143d788bd7ded0eca", "shasum": "" }, "require": { @@ -12727,13 +12130,13 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.57.1", - "illuminate/view": "^10.48.10", - "larastan/larastan": "^2.9.6", + "friendsofphp/php-cs-fixer": "^3.59.3", + "illuminate/view": "^10.48.12", + "larastan/larastan": "^2.9.7", "laravel-zero/framework": "^10.4.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^1.15.1", - "pestphp/pest": "^2.34.7" + "pestphp/pest": "^2.34.8" }, "bin": [ "builds/pint" @@ -12769,7 +12172,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2024-05-21T18:08:25+00:00" + "time": "2024-07-09T15:58:08+00:00" }, { "name": "mockery/mockery", @@ -12856,16 +12259,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -12873,11 +12276,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -12903,7 +12307,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -12911,44 +12315,42 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nunomaduro/collision", - "version": "v7.10.0", + "version": "v8.1.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "49ec67fa7b002712da8526678abd651c09f375b2" + "reference": "13e5d538b95a744d85f447a321ce10adb28e9af9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/49ec67fa7b002712da8526678abd651c09f375b2", - "reference": "49ec67fa7b002712da8526678abd651c09f375b2", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/13e5d538b95a744d85f447a321ce10adb28e9af9", + "reference": "13e5d538b95a744d85f447a321ce10adb28e9af9", "shasum": "" }, "require": { - "filp/whoops": "^2.15.3", - "nunomaduro/termwind": "^1.15.1", - "php": "^8.1.0", - "symfony/console": "^6.3.4" + "filp/whoops": "^2.15.4", + "nunomaduro/termwind": "^2.0.1", + "php": "^8.2.0", + "symfony/console": "^7.0.4" }, "conflict": { - "laravel/framework": ">=11.0.0" + "laravel/framework": "<11.0.0 || >=12.0.0", + "phpunit/phpunit": "<10.5.1 || >=12.0.0" }, "require-dev": { - "brianium/paratest": "^7.3.0", - "laravel/framework": "^10.28.0", - "laravel/pint": "^1.13.3", - "laravel/sail": "^1.25.0", - "laravel/sanctum": "^3.3.1", - "laravel/tinker": "^2.8.2", - "nunomaduro/larastan": "^2.6.4", - "orchestra/testbench-core": "^8.13.0", - "pestphp/pest": "^2.23.2", - "phpunit/phpunit": "^10.4.1", - "sebastian/environment": "^6.0.1", - "spatie/laravel-ignition": "^2.3.1" + "larastan/larastan": "^2.9.2", + "laravel/framework": "^11.0.0", + "laravel/pint": "^1.14.0", + "laravel/sail": "^1.28.2", + "laravel/sanctum": "^4.0.0", + "laravel/tinker": "^2.9.0", + "orchestra/testbench-core": "^9.0.0", + "pestphp/pest": "^2.34.1 || ^3.0.0", + "sebastian/environment": "^6.0.1 || ^7.0.0" }, "type": "library", "extra": { @@ -12956,6 +12358,9 @@ "providers": [ "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" } }, "autoload": { @@ -13007,20 +12412,20 @@ "type": "patreon" } ], - "time": "2023-10-11T15:45:01+00:00" + "time": "2024-03-06T16:20:09+00:00" }, { "name": "pestphp/pest", - "version": "v2.34.7", + "version": "v2.34.8", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "a7a3e4240e341d0fee1c54814ce18adc26ce5a76" + "reference": "e8f122bf47585c06431e0056189ec6bfd6f41f57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/a7a3e4240e341d0fee1c54814ce18adc26ce5a76", - "reference": "a7a3e4240e341d0fee1c54814ce18adc26ce5a76", + "url": "https://api.github.com/repos/pestphp/pest/zipball/e8f122bf47585c06431e0056189ec6bfd6f41f57", + "reference": "e8f122bf47585c06431e0056189ec6bfd6f41f57", "shasum": "" }, "require": { @@ -13039,8 +12444,8 @@ }, "require-dev": { "pestphp/pest-dev-tools": "^2.16.0", - "pestphp/pest-plugin-type-coverage": "^2.8.1", - "symfony/process": "^6.4.0|^7.0.4" + "pestphp/pest-plugin-type-coverage": "^2.8.3", + "symfony/process": "^6.4.0|^7.1.1" }, "bin": [ "bin/pest" @@ -13103,7 +12508,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v2.34.7" + "source": "https://github.com/pestphp/pest/tree/v2.34.8" }, "funding": [ { @@ -13115,7 +12520,7 @@ "type": "github" } ], - "time": "2024-04-05T07:44:17+00:00" + "time": "2024-06-10T22:02:16+00:00" }, { "name": "pestphp/pest-plugin", @@ -13508,16 +12913,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.14", + "version": "10.1.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" + "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", - "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", + "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", "shasum": "" }, "require": { @@ -13574,7 +12979,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.15" }, "funding": [ { @@ -13582,7 +12987,7 @@ "type": "github" } ], - "time": "2024-03-12T15:33:41+00:00" + "time": "2024-06-29T08:25:15+00:00" }, { "name": "phpunit/php-file-iterator", @@ -14890,23 +14295,97 @@ "time": "2022-05-20T15:13:10+00:00" }, { - "name": "spatie/flare-client-php", - "version": "1.6.0", + "name": "spatie/error-solutions", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/spatie/flare-client-php.git", - "reference": "220a7c8745e9fa427d54099f47147c4b97fe6462" + "url": "https://github.com/spatie/error-solutions.git", + "reference": "4bb6c734dc992b2db3e26df1ef021c75d2218b13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/220a7c8745e9fa427d54099f47147c4b97fe6462", - "reference": "220a7c8745e9fa427d54099f47147c4b97fe6462", + "url": "https://api.github.com/repos/spatie/error-solutions/zipball/4bb6c734dc992b2db3e26df1ef021c75d2218b13", + "reference": "4bb6c734dc992b2db3e26df1ef021c75d2218b13", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "illuminate/broadcasting": "^10.0|^11.0", + "illuminate/cache": "^10.0|^11.0", + "illuminate/support": "^10.0|^11.0", + "livewire/livewire": "^2.11|^3.3.5", + "openai-php/client": "^0.10.1", + "orchestra/testbench": "^7.0|8.22.3|^9.0", + "pestphp/pest": "^2.20", + "phpstan/phpstan": "^1.11", + "psr/simple-cache": "^3.0", + "psr/simple-cache-implementation": "^3.0", + "spatie/ray": "^1.28", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "legacy/ignition", + "Spatie\\ErrorSolutions\\": "src", + "Spatie\\LaravelIgnition\\": "legacy/laravel-ignition" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ruben Van Assche", + "email": "ruben@spatie.be", + "role": "Developer" + } + ], + "description": "This is my package error-solutions", + "homepage": "https://github.com/spatie/error-solutions", + "keywords": [ + "error-solutions", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/error-solutions/issues", + "source": "https://github.com/spatie/error-solutions/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/Spatie", + "type": "github" + } + ], + "time": "2024-07-09T12:13:32+00:00" + }, + { + "name": "spatie/flare-client-php", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/flare-client-php.git", + "reference": "097040ff51e660e0f6fc863684ac4b02c93fa234" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/097040ff51e660e0f6fc863684ac4b02c93fa234", + "reference": "097040ff51e660e0f6fc863684ac4b02c93fa234", "shasum": "" }, "require": { "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0", "php": "^8.0", - "spatie/backtrace": "^1.5.2", + "spatie/backtrace": "^1.6.1", "symfony/http-foundation": "^5.2|^6.0|^7.0", "symfony/mime": "^5.2|^6.0|^7.0", "symfony/process": "^5.2|^6.0|^7.0", @@ -14948,7 +14427,7 @@ ], "support": { "issues": "https://github.com/spatie/flare-client-php/issues", - "source": "https://github.com/spatie/flare-client-php/tree/1.6.0" + "source": "https://github.com/spatie/flare-client-php/tree/1.7.0" }, "funding": [ { @@ -14956,28 +14435,28 @@ "type": "github" } ], - "time": "2024-05-22T09:45:39+00:00" + "time": "2024-06-12T14:39:14+00:00" }, { "name": "spatie/ignition", - "version": "1.14.1", + "version": "1.15.0", "source": { "type": "git", "url": "https://github.com/spatie/ignition.git", - "reference": "c23cc018c5f423d2f413b99f84655fceb6549811" + "reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/ignition/zipball/c23cc018c5f423d2f413b99f84655fceb6549811", - "reference": "c23cc018c5f423d2f413b99f84655fceb6549811", + "url": "https://api.github.com/repos/spatie/ignition/zipball/e3a68e137371e1eb9edc7f78ffa733f3b98991d2", + "reference": "e3a68e137371e1eb9edc7f78ffa733f3b98991d2", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", "php": "^8.0", - "spatie/backtrace": "^1.5.3", - "spatie/flare-client-php": "^1.4.0", + "spatie/error-solutions": "^1.0", + "spatie/flare-client-php": "^1.7", "symfony/console": "^5.4|^6.0|^7.0", "symfony/var-dumper": "^5.4|^6.0|^7.0" }, @@ -15039,20 +14518,20 @@ "type": "github" } ], - "time": "2024-05-03T15:56:16+00:00" + "time": "2024-06-12T14:55:22+00:00" }, { "name": "spatie/laravel-ignition", - "version": "2.7.0", + "version": "2.8.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ignition.git", - "reference": "f52124d50122611e8a40f628cef5c19ff6cc5b57" + "reference": "3c067b75bfb50574db8f7e2c3978c65eed71126c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/f52124d50122611e8a40f628cef5c19ff6cc5b57", - "reference": "f52124d50122611e8a40f628cef5c19ff6cc5b57", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/3c067b75bfb50574db8f7e2c3978c65eed71126c", + "reference": "3c067b75bfb50574db8f7e2c3978c65eed71126c", "shasum": "" }, "require": { @@ -15061,8 +14540,7 @@ "ext-mbstring": "*", "illuminate/support": "^10.0|^11.0", "php": "^8.1", - "spatie/flare-client-php": "^1.5", - "spatie/ignition": "^1.14", + "spatie/ignition": "^1.15", "symfony/console": "^6.2.3|^7.0", "symfony/var-dumper": "^6.2.3|^7.0" }, @@ -15131,7 +14609,178 @@ "type": "github" } ], - "time": "2024-05-02T13:42:49+00:00" + "time": "2024-06-12T15:01:18+00:00" + }, + { + "name": "symfony/http-client", + "version": "v6.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "6e9db0025db565bcf8f1d46ed734b549e51e6045" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/6e9db0025db565bcf8f1d46ed734b549e51e6045", + "reference": "6e9db0025db565bcf8f1d46ed734b549e51e6045", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "^3.4.1", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-06-28T07:59:05+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "20414d96f391677bf80078aa55baece78b82647d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/20414d96f391677bf80078aa55baece78b82647d", + "reference": "20414d96f391677bf80078aa55baece78b82647d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", diff --git a/config/constants.php b/config/constants.php index 444d144a8..861b645ed 100644 --- a/config/constants.php +++ b/config/constants.php @@ -22,8 +22,8 @@ ], 'services' => [ // Temporary disabled until cache is implemented - 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', - // 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', + // 'official' => 'https://cdn.coollabs.io/coolify/service-templates.json', + 'official' => 'https://raw.githubusercontent.com/coollabsio/coolify/main/templates/service-templates.json', ], 'limits' => [ 'trial_period' => 0, diff --git a/config/coolify.php b/config/coolify.php index c7cfe6101..a6d6d8581 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -14,5 +14,4 @@ 'helper_image' => env('HELPER_IMAGE', 'ghcr.io/coollabsio/coolify-helper:latest'), 'is_horizon_enabled' => env('HORIZON_ENABLED', true), 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), - 'is_sentinel_enabled' => env('SENTINEL_ENABLED', false), ]; diff --git a/config/horizon.php b/config/horizon.php index ef7df3f1b..939d74883 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -182,7 +182,7 @@ 'defaults' => [ 's6' => [ 'connection' => 'redis', - 'queue' => ['default'], + 'queue' => ['high', 'default'], 'balance' => env('HORIZON_BALANCE', 'auto'), 'maxTime' => 0, 'maxJobs' => 0, diff --git a/config/sanctum.php b/config/sanctum.php index 529cfdc99..f1e5fc0e5 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -60,8 +60,9 @@ */ 'middleware' => [ - 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, - 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, + 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, + 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, ], ]; diff --git a/config/sentry.php b/config/sentry.php index 33a24edfb..3ce5dfbc9 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ // 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.297', + 'release' => '4.0.0-beta.308', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/session.php b/config/session.php index c7b176a5a..44ca7ded9 100644 --- a/config/session.php +++ b/config/session.php @@ -18,7 +18,7 @@ | */ - 'driver' => env('SESSION_DRIVER', 'redis'), + 'driver' => env('SESSION_DRIVER', 'database'), /* |-------------------------------------------------------------------------- diff --git a/config/subscription.php b/config/subscription.php index 07665075f..3e0182de9 100644 --- a/config/subscription.php +++ b/config/subscription.php @@ -1,7 +1,8 @@ env('SUBSCRIPTION_PROVIDER', null), // stripe, paddle, lemon + 'provider' => env('SUBSCRIPTION_PROVIDER', null), // stripe + // Stripe 'stripe_api_key' => env('STRIPE_API_KEY', null), 'stripe_webhook_secret' => env('STRIPE_WEBHOOK_SECRET', null), @@ -22,29 +23,4 @@ 'stripe_price_id_dynamic_monthly' => env('STRIPE_PRICE_ID_DYNAMIC_MONTHLY', null), 'stripe_price_id_dynamic_yearly' => env('STRIPE_PRICE_ID_DYNAMIC_YEARLY', null), - - // Paddle - 'paddle_vendor_id' => env('PADDLE_VENDOR_ID', null), - 'paddle_vendor_auth_code' => env('PADDLE_VENDOR_AUTH_CODE', null), - 'paddle_webhook_secret' => env('PADDLE_WEBHOOK_SECRET', null), - 'paddle_public_key' => env('PADDLE_PUBLIC_KEY', null), - 'paddle_price_id_basic_monthly' => env('PADDLE_PRICE_ID_BASIC_MONTHLY', null), - 'paddle_price_id_basic_yearly' => env('PADDLE_PRICE_ID_BASIC_YEARLY', null), - 'paddle_price_id_pro_monthly' => env('PADDLE_PRICE_ID_PRO_MONTHLY', null), - 'paddle_price_id_pro_yearly' => env('PADDLE_PRICE_ID_PRO_YEARLY', null), - 'paddle_price_id_ultimate_monthly' => env('PADDLE_PRICE_ID_ULTIMATE_MONTHLY', null), - 'paddle_price_id_ultimate_yearly' => env('PADDLE_PRICE_ID_ULTIMATE_YEARLY', null), - - // Lemon - 'lemon_squeezy_api_key' => env('LEMON_SQUEEZY_API_KEY', null), - 'lemon_squeezy_webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', null), - 'lemon_squeezy_checkout_id_basic_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_BASIC_MONTHLY', null), - 'lemon_squeezy_checkout_id_basic_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_BASIC_YEARLY', null), - 'lemon_squeezy_checkout_id_pro_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_PRO_MONTHLY', null), - 'lemon_squeezy_checkout_id_pro_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_PRO_YEARLY', null), - 'lemon_squeezy_checkout_id_ultimate_monthly' => env('LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_MONTHLY', null), - 'lemon_squeezy_checkout_id_ultimate_yearly' => env('LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_YEARLY', null), - 'lemon_squeezy_basic_plan_ids' => env('LEMON_SQUEEZY_BASIC_PLAN_IDS', ''), - 'lemon_squeezy_pro_plan_ids' => env('LEMON_SQUEEZY_PRO_PLAN_IDS', ''), - 'lemon_squeezy_ultimate_plan_ids' => env('LEMON_SQUEEZY_ULTIMATE_PLAN_IDS', ''), ]; diff --git a/config/version.php b/config/version.php index 06c1e6c66..4e1a16799 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ dropColumn('is_metrics_enabled'); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->boolean('is_metrics_enabled')->default(false); + $table->integer('metrics_refresh_rate_seconds')->default(5); + $table->integer('metrics_history_days')->default(30); + $table->string('metrics_token')->default(generateSentinelToken()); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->boolean('is_metrics_enabled')->default(true); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_metrics_enabled'); + $table->dropColumn('metrics_refresh_rate_seconds'); + $table->dropColumn('metrics_history_days'); + $table->dropColumn('metrics_token'); + }); + } +}; diff --git a/database/migrations/2024_06_20_102551_add_server_api_sentinel.php b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php new file mode 100644 index 000000000..b840195af --- /dev/null +++ b/database/migrations/2024_06_20_102551_add_server_api_sentinel.php @@ -0,0 +1,28 @@ +boolean('is_server_api_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_server_api_enabled'); + }); + } +}; diff --git a/database/migrations/2024_06_21_143358_add_api_deployment_type.php b/database/migrations/2024_06_21_143358_add_api_deployment_type.php new file mode 100644 index 000000000..03f4d4796 --- /dev/null +++ b/database/migrations/2024_06_21_143358_add_api_deployment_type.php @@ -0,0 +1,28 @@ +boolean('is_api')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('is_api'); + }); + } +}; diff --git a/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php b/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php new file mode 100644 index 000000000..1687e047c --- /dev/null +++ b/database/migrations/2024_06_22_081140_alter_instance_settings_add_instance_name.php @@ -0,0 +1,28 @@ +string('instance_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('instance_name'); + }); + } +}; diff --git a/database/migrations/2024_06_25_184323_update_db.php b/database/migrations/2024_06_25_184323_update_db.php new file mode 100644 index 000000000..005e063cc --- /dev/null +++ b/database/migrations/2024_06_25_184323_update_db.php @@ -0,0 +1,92 @@ +dropColumn('docker_compose_pr_location'); + $table->dropColumn('docker_compose_pr'); + $table->dropColumn('docker_compose_pr_raw'); + }); + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('lemon_subscription_id'); + $table->dropColumn('lemon_order_id'); + $table->dropColumn('lemon_product_id'); + $table->dropColumn('lemon_variant_id'); + $table->dropColumn('lemon_variant_name'); + $table->dropColumn('lemon_customer_id'); + $table->dropColumn('lemon_status'); + $table->dropColumn('lemon_renews_at'); + $table->dropColumn('lemon_update_payment_menthod_url'); + $table->dropColumn('lemon_trial_ends_at'); + $table->dropColumn('lemon_ends_at'); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->string('uuid')->nullable()->after('id'); + }); + + EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) { + $environmentVariable->update([ + 'uuid' => (string) new Cuid2(), + ]); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->string('uuid')->nullable(false)->change(); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->integer('metrics_history_days')->default(7)->change(); + }); + Server::all()->each(function (Server $server) { + $server->settings->update([ + 'metrics_history_days' => 7, + ]); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->string('docker_compose_pr_location')->nullable()->default('/docker-compose.yaml')->after('docker_compose_location'); + $table->longText('docker_compose_pr')->nullable()->after('docker_compose_location'); + $table->longText('docker_compose_pr_raw')->nullable()->after('docker_compose'); + }); + Schema::table('subscriptions', function (Blueprint $table) { + $table->string('lemon_subscription_id')->nullable()->after('stripe_subscription_id'); + $table->string('lemon_order_id')->nullable()->after('lemon_subscription_id'); + $table->string('lemon_product_id')->nullable()->after('lemon_order_id'); + $table->string('lemon_variant_id')->nullable()->after('lemon_product_id'); + $table->string('lemon_variant_name')->nullable()->after('lemon_variant_id'); + $table->string('lemon_customer_id')->nullable()->after('lemon_variant_name'); + $table->string('lemon_status')->nullable()->after('lemon_customer_id'); + $table->timestamp('lemon_renews_at')->nullable()->after('lemon_status'); + $table->string('lemon_update_payment_menthod_url')->nullable()->after('lemon_renews_at'); + $table->timestamp('lemon_trial_ends_at')->nullable()->after('lemon_update_payment_menthod_url'); + $table->timestamp('lemon_ends_at')->nullable()->after('lemon_trial_ends_at'); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('uuid'); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->integer('metrics_history_days')->default(30)->change(); + }); + Server::all()->each(function (Server $server) { + $server->settings->update([ + 'metrics_history_days' => 30, + ]); + }); + } +}; diff --git a/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php new file mode 100644 index 000000000..b319adb70 --- /dev/null +++ b/database/migrations/2024_07_01_115528_add_is_api_allowed_and_iplist.php @@ -0,0 +1,24 @@ +boolean('is_api_enabled')->default(true); + $table->text('allowed_ips')->nullable(); + }); + } + + public function down(): void + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('is_api_enabled'); + $table->dropColumn('allowed_ips'); + }); + } +}; diff --git a/database/migrations/2024_07_05_120217_remove_unique_from_tag_names.php b/database/migrations/2024_07_05_120217_remove_unique_from_tag_names.php new file mode 100644 index 000000000..301de814b --- /dev/null +++ b/database/migrations/2024_07_05_120217_remove_unique_from_tag_names.php @@ -0,0 +1,28 @@ +dropUnique(['name']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tags', function (Blueprint $table) { + $table->unique(['name']); + }); + } +}; diff --git a/database/seeders/EnvironmentSeeder.php b/database/seeders/EnvironmentSeeder.php index 0e980f22b..1c6d562a9 100644 --- a/database/seeders/EnvironmentSeeder.php +++ b/database/seeders/EnvironmentSeeder.php @@ -9,7 +9,5 @@ class EnvironmentSeeder extends Seeder /** * Run the database seeds. */ - public function run(): void - { - } + public function run(): void {} } diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 357138ca1..b8156cab5 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -69,27 +69,6 @@ services: - STRIPE_PRICE_ID_ULTIMATE_MONTHLY_OLD - STRIPE_PRICE_ID_ULTIMATE_YEARLY_OLD - STRIPE_EXCLUDED_PLANS - - PADDLE_VENDOR_ID - - PADDLE_WEBHOOK_SECRET - - PADDLE_VENDOR_AUTH_CODE - - PADDLE_PUBLIC_KEY - - PADDLE_PRICE_ID_BASIC_MONTHLY - - PADDLE_PRICE_ID_BASIC_YEARLY - - PADDLE_PRICE_ID_PRO_MONTHLY - - PADDLE_PRICE_ID_PRO_YEARLY - - PADDLE_PRICE_ID_ULTIMATE_MONTHLY - - PADDLE_PRICE_ID_ULTIMATE_YEARLY - - LEMON_SQUEEZY_API_KEY - - LEMON_SQUEEZY_WEBHOOK_SECRET - - LEMON_SQUEEZY_CHECKOUT_ID_BASIC_MONTHLY - - LEMON_SQUEEZY_CHECKOUT_ID_BASIC_YEARLY - - LEMON_SQUEEZY_CHECKOUT_ID_PRO_MONTHLY - - LEMON_SQUEEZY_CHECKOUT_ID_PRO_YEARLY - - LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_MONTHLY - - LEMON_SQUEEZY_CHECKOUT_ID_ULTIMATE_YEARLY - - LEMON_SQUEEZY_BASIC_PLAN_IDS - - LEMON_SQUEEZY_PRO_PLAN_IDS - - LEMON_SQUEEZY_ULTIMATE_PLAN_IDS ports: - "${APP_PORT:-8000}:80" expose: diff --git a/docker/dev/etc/s6-overlay/s6-rc.d/init-setup/up b/docker/dev/etc/s6-overlay/s6-rc.d/init-setup/up index e974e54cc..e02307e49 100644 --- a/docker/dev/etc/s6-overlay/s6-rc.d/init-setup/up +++ b/docker/dev/etc/s6-overlay/s6-rc.d/init-setup/up @@ -1,5 +1,5 @@ #!/command/execlineb -P foreground { composer -d /var/www/html/ install } foreground { php /var/www/html/artisan migrate --step } -foreground { php /var/www/html/artisan dev:init } +foreground { php /var/www/html/artisan dev --init } diff --git a/lang/ar.json b/lang/ar.json new file mode 100644 index 000000000..c5ec96c8d --- /dev/null +++ b/lang/ar.json @@ -0,0 +1,30 @@ +{ + "auth.login": "تسجيل الدخول", + "auth.login.azure": "تسجيل الدخول باستخدام Microsoft", + "auth.login.bitbucket": "تسجيل الدخول باستخدام Bitbucket", + "auth.login.github": "تسجيل الدخول باستخدام GitHub", + "auth.login.gitlab": "تسجيل الدخول باستخدام Gitlab", + "auth.login.google": "تسجيل الدخول باستخدام Google", + "auth.already_registered": "هل سبق لك التسجيل؟", + "auth.confirm_password": "تأكيد كلمة المرور", + "auth.forgot_password": "نسيت كلمة المرور", + "auth.forgot_password_send_email": "إرسال بريد إلكتروني لإعادة تعيين كلمة المرور", + "auth.register_now": "تسجيل", + "auth.logout": "تسجيل الخروج", + "auth.register": "تسجيل", + "auth.registration_disabled": "تم تعطيل التسجيل. يرجى التواصل مع المسؤول.", + "auth.reset_password": "إعادة تعيين كلمة المرور", + "auth.failed": "هذه البيانات لا تتطابق مع سجلاتنا.", + "auth.failed.callback": "فشل في معالجة استدعاء من مزود تسجيل الدخول.", + "auth.failed.password": "كلمة المرور المقدمة غير صحيحة.", + "auth.failed.email": "لا يمكننا العثور على مستخدم بهذا البريد الإلكتروني.", + "auth.throttle": "عدد محاولات تسجيل الدخول كثيرة جدًا. يرجى المحاولة مرة أخرى في :seconds ثانية.", + "input.name": "الاسم", + "input.email": "البريد الإلكتروني", + "input.password": "كلمة المرور", + "input.password.again": "كلمة المرور مرة أخرى", + "input.code": "الرمز لمرة واحدة", + "input.recovery_code": "رمز الاسترداد", + "button.save": "حفظ", + "repository.url": "أمثلة