diff --git a/README.md b/README.md index eb66299ac2..97580b875c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)! ## Github Sponsors ($40+) BC Direct +SerpAPI typebot QuantCDN diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index 8f4bfdf251..f0e1de8f6c 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -6,14 +6,12 @@ use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; use App\Actions\Shared\ComplexStatusCheck; -use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\Server; use App\Models\ServiceDatabase; use App\Notifications\Container\ContainerRestarted; use App\Notifications\Container\ContainerStopped; use Illuminate\Support\Arr; -use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; class GetContainersStatus @@ -24,9 +22,9 @@ class GetContainersStatus public function handle(Server $server) { - if (isDev()) { - $server = Server::find(0); - } + // if (isDev()) { + // $server = Server::find(0); + // } $this->server = $server; if (!$this->server->isFunctional()) { return 'Server is not ready.'; @@ -154,7 +152,7 @@ private function sentinel() if ($isPublic) { $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { if ($this->server->isSwarm()) { - // TODO: fix this with sentinel + // TODO: fix this with sentinel return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; } else { return data_get($value, 'name') === "$uuid-proxy"; @@ -316,7 +314,7 @@ private function sentinel() $this->server->proxyType(); $foundProxyContainer = $containers->filter(function ($value, $key) { if ($this->server->isSwarm()) { - // TODO: fix this with sentinel + // TODO: fix this with sentinel return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; } else { return data_get($value, 'name') === 'coolify-proxy'; @@ -442,19 +440,21 @@ private function old_way() if ($database_id) { $service_db = ServiceDatabase::where('id', $database_id)->first(); if ($service_db) { - $uuid = $service_db->service->uuid; - $isPublic = data_get($service_db, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; + $uuid = data_get($service_db, 'service.uuid'); + if ($uuid) { + $isPublic = data_get($service_db, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + })->first(); + if (!$foundTcpProxy) { + StartDatabaseProxy::run($service_db); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); } - })->first(); - if (!$foundTcpProxy) { - StartDatabaseProxy::run($service_db); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); } } } diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 6e2ad06b01..a28f85901f 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -8,14 +8,13 @@ use App\Models\Application; use App\Models\Service; use App\Models\Team; +use App\Notifications\ScheduledTask\TaskFailed; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Collection; -use Throwable; class ScheduledTaskJob implements ShouldQueue { @@ -114,6 +113,7 @@ public function handle(): void 'message' => $this->task_output ?? $e->getMessage(), ]); } + $this->team?->notify(new TaskFailed($this->task, $e->getMessage())); // send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage()); throw $e; } diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index 31f54f6d69..ddd6bd271c 100644 --- a/app/Jobs/SendMessageToDiscordJob.php +++ b/app/Jobs/SendMessageToDiscordJob.php @@ -41,7 +41,6 @@ public function handle(): void $payload = [ 'content' => $this->text, ]; - ray($payload); Http::post($this->webhookUrl, $payload); } } diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 2681b69e0e..8f4e870905 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -52,6 +52,9 @@ class Index extends Component public function mount() { + if (auth()->user()?->isMember() && auth()->user()->currentTeam()->show_boarding === true) { + return redirect()->route('dashboard'); + } $this->privateKeyName = generate_random_name(); $this->remoteServerName = generate_random_name(); if (isDev()) { diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index 8aad8ccf0b..88705437be 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -16,6 +16,7 @@ class Discord extends Component 'team.discord_notifications_deployments' => 'nullable|boolean', 'team.discord_notifications_status_changes' => 'nullable|boolean', 'team.discord_notifications_database_backups' => 'nullable|boolean', + 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean', ]; protected $validationAttributes = [ 'team.discord_webhook_url' => 'Discord Webhook', diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 343cbda3ec..6ef9b22553 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -28,6 +28,7 @@ class Email extends Component 'team.smtp_notifications_deployments' => 'nullable|boolean', 'team.smtp_notifications_status_changes' => 'nullable|boolean', 'team.smtp_notifications_database_backups' => 'nullable|boolean', + 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean', 'team.use_instance_email_settings' => 'boolean', 'team.resend_enabled' => 'nullable|boolean', 'team.resend_api_key' => 'nullable', diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index 35b8685274..685c9e8ebb 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -18,10 +18,12 @@ class Telegram extends Component 'team.telegram_notifications_deployments' => 'nullable|boolean', 'team.telegram_notifications_status_changes' => 'nullable|boolean', 'team.telegram_notifications_database_backups' => 'nullable|boolean', + 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean', 'team.telegram_notifications_test_message_thread_id' => 'nullable|string', 'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string', 'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string', 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string', + 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string', ]; protected $validationAttributes = [ 'team.telegram_token' => 'Token', diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 66c59cff50..718312d2dd 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -31,7 +31,7 @@ class General extends Component public ?string $initialDockerComposeLocation = null; public ?string $initialDockerComposePrLocation = null; - public $parsedServices = []; + public null|Collection $parsedServices; public $parsedServiceDomains = []; protected $listeners = [ @@ -118,6 +118,10 @@ public function mount() { try { $this->parsedServices = $this->application->parseCompose(); + if (is_null($this->parsedServices) || empty($this->parsedServices)) { + $this->dispatch('error', "Failed to parse your docker-compose file. Please check the syntax and try again."); + return; + } } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } @@ -160,6 +164,10 @@ public function loadComposeFile($isInit = false) return; } ['parsedServices' => $this->parsedServices, 'initialDockerComposeLocation' => $this->initialDockerComposeLocation, 'initialDockerComposePrLocation' => $this->initialDockerComposePrLocation] = $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."); + return; + } $compose = $this->application->parseCompose(); $services = data_get($compose, 'services'); if ($services) { diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 3705d6f933..8ea77950e4 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -12,7 +12,6 @@ class Create extends Component public $type; public function mount() { - $services = getServiceTemplates(); $type = str(request()->query('type')); $destination_uuid = request()->query('destination'); $server_id = request()->query('server_id'); @@ -25,83 +24,87 @@ public function mount() if (!$environment) { return redirect()->route('dashboard'); } - if (in_array($type, DATABASE_TYPES)) { - if ($type->value() === "postgresql") { - $database = create_standalone_postgresql($environment->id, $destination_uuid); - } else if ($type->value() === 'redis') { - $database = create_standalone_redis($environment->id, $destination_uuid); - } else if ($type->value() === 'mongodb') { - $database = create_standalone_mongodb($environment->id, $destination_uuid); - } else if ($type->value() === 'mysql') { - $database = create_standalone_mysql($environment->id, $destination_uuid); - } else if ($type->value() === 'mariadb') { - $database = create_standalone_mariadb($environment->id, $destination_uuid); - } else if ($type->value() === 'keydb') { - $database = create_standalone_keydb($environment->id, $destination_uuid); - } else if ($type->value() === 'dragonfly') { - $database = create_standalone_dragonfly($environment->id, $destination_uuid); - } else if ($type->value() === 'clickhouse') { - $database = create_standalone_clickhouse($environment->id, $destination_uuid); - } - return redirect()->route('project.database.configuration', [ - 'project_uuid' => $project->uuid, - 'environment_name' => $environment->name, - 'database_uuid' => $database->uuid, - ]); - } - if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) { - $oneClickServiceName = $type->after('one-click-service-')->value(); - $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) { - $destination = StandaloneDocker::whereUuid($destination_uuid)->first(); - $service_payload = [ - 'name' => "$oneClickServiceName-" . str()->random(10), - 'docker_compose_raw' => base64_decode($oneClickService), - 'environment_id' => $environment->id, - 'service_type' => $oneClickServiceName, - 'server_id' => (int) $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, - ]); - }); + if (isset($type) && isset($destination_uuid) && isset($server_id)) { + $services = getServiceTemplates(); + + if (in_array($type, DATABASE_TYPES)) { + if ($type->value() === "postgresql") { + $database = create_standalone_postgresql($environment->id, $destination_uuid); + } else if ($type->value() === 'redis') { + $database = create_standalone_redis($environment->id, $destination_uuid); + } else if ($type->value() === 'mongodb') { + $database = create_standalone_mongodb($environment->id, $destination_uuid); + } else if ($type->value() === 'mysql') { + $database = create_standalone_mysql($environment->id, $destination_uuid); + } else if ($type->value() === 'mariadb') { + $database = create_standalone_mariadb($environment->id, $destination_uuid); + } else if ($type->value() === 'keydb') { + $database = create_standalone_keydb($environment->id, $destination_uuid); + } else if ($type->value() === 'dragonfly') { + $database = create_standalone_dragonfly($environment->id, $destination_uuid); + } else if ($type->value() === 'clickhouse') { + $database = create_standalone_clickhouse($environment->id, $destination_uuid); } - $service->parse(isNew: true); - return redirect()->route('project.service.configuration', [ - 'service_uuid' => $service->uuid, - 'environment_name' => $environment->name, + return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, + 'environment_name' => $environment->name, + 'database_uuid' => $database->uuid, ]); } + if ($type->startsWith('one-click-service-') && !is_null((int)$server_id)) { + $oneClickServiceName = $type->after('one-click-service-')->value(); + $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) { + $destination = StandaloneDocker::whereUuid($destination_uuid)->first(); + $service_payload = [ + 'name' => "$oneClickServiceName-" . str()->random(10), + 'docker_compose_raw' => base64_decode($oneClickService), + 'environment_id' => $environment->id, + 'service_type' => $oneClickServiceName, + 'server_id' => (int) $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); + return redirect()->route('project.service.configuration', [ + 'service_uuid' => $service->uuid, + 'environment_name' => $environment->name, + 'project_uuid' => $project->uuid, + ]); + } + } + $this->type = $type->value(); } - $this->type = $type->value(); } public function render() { diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php new file mode 100644 index 0000000000..12546ff1b5 --- /dev/null +++ b/app/Livewire/Team/AdminView.php @@ -0,0 +1,117 @@ +route('dashboard'); + } + $this->getUsers(); + } + public function submitSearch() + { + if ($this->search !== "") { + $this->users = User::where(function ($query) { + $query->where('name', 'like', "%{$this->search}%") + ->orWhere('email', 'like', "%{$this->search}%"); + })->get()->filter(function ($user) { + return $user->id !== auth()->id(); + }); + } else { + $this->getUsers(); + } + } + public function getUsers() + { + $this->users = User::where('id', '!=', auth()->id())->get(); + // $this->users = User::all(); + } + private function finalizeDeletion(User $user, Team $team) + { + $servers = $team->servers; + foreach ($servers as $server) { + $resources = $server->definedResources(); + foreach ($resources as $resource) { + ray("Deleting resource: " . $resource->name); + $resource->forceDelete(); + } + ray("Deleting server: " . $server->name); + $server->forceDelete(); + } + + $projects = $team->projects; + foreach ($projects as $project) { + ray("Deleting project: " . $project->name); + $project->forceDelete(); + } + $team->members()->detach($user->id); + ray('Deleting team: ' . $team->name); + $team->delete(); + } + public function delete($id) + { + $user = User::find($id); + $teams = $user->teams; + foreach ($teams as $team) { + ray($team->name); + $user_alone_in_team = $team->members->count() === 1; + if ($team->id === 0) { + if ($user_alone_in_team) { + ray('user is alone in the root team, do nothing'); + return $this->dispatch('error', 'User is alone in the root team, cannot delete'); + } + } + if ($user_alone_in_team) { + ray('user is alone in the team'); + $this->finalizeDeletion($user, $team); + continue; + } + ray('user is not alone in the team'); + if ($user->isOwner()) { + $found_other_owner_or_admin = $team->members->filter(function ($member) { + return $member->pivot->role === 'owner' || $member->pivot->role === 'admin'; + })->where('id', '!=', $user->id)->first(); + + if ($found_other_owner_or_admin) { + ray('found other owner or admin'); + $team->members()->detach($user->id); + continue; + } else { + $found_other_member_who_is_not_owner = $team->members->filter(function ($member) { + return $member->pivot->role === 'member'; + })->first(); + if ($found_other_member_who_is_not_owner) { + ray('found other member who is not owner'); + $found_other_member_who_is_not_owner->pivot->role = 'owner'; + $found_other_member_who_is_not_owner->pivot->save(); + $team->members()->detach($user->id); + } else { + // This should never happen as if the user is the only member in the team, the team should be deleted already. + ray('found no other member who is not owner'); + $this->finalizeDeletion($user, $team); + } + continue; + } + } else { + ray('user is not owner'); + $team->members()->detach($user->id); + } + } + ray("Deleting user: " . $user->name); + $user->delete(); + $this->getUsers(); + } + public function render() + { + return view('livewire.team.admin-view'); + } +} diff --git a/app/Models/Application.php b/app/Models/Application.php index 3d0b92aafa..0f3425dd60 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -113,6 +113,18 @@ public function link() } return null; } + public function failedTaskLink($task_uuid) + { + if (data_get($this, 'environment.project.uuid')) { + return route('project.application.scheduled-tasks', [ + 'project_uuid' => data_get($this, 'environment.project.uuid'), + 'environment_name' => data_get($this, 'environment.name'), + 'application_uuid' => data_get($this, 'uuid'), + 'task_uuid' => $task_uuid + ]); + } + return null; + } public function settings() { return $this->hasOne(ApplicationSetting::class); @@ -879,7 +891,7 @@ function loadComposeFile($isInit = false) if (!$composeFileContent) { $this->docker_compose_location = $initialDockerComposeLocation; $this->save(); - throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile"); + throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile

Check if you used the right extension (.yaml or .yml) in the compose file name."); } else { $this->docker_compose_raw = $composeFileContent; $this->save(); diff --git a/app/Models/Environment.php b/app/Models/Environment.php index 7ed9e38e59..a1f3e41904 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -8,6 +8,18 @@ class Environment extends Model { protected $guarded = []; + + protected static function booted() + { + static::deleting(function ($environment) { + $shared_variables = $environment->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting environment shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } + + }); + } public function isEmpty() { return $this->applications()->count() == 0 && diff --git a/app/Models/Project.php b/app/Models/Project.php index 2621d3da19..c2be8cc32c 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -25,6 +25,11 @@ protected static function booted() static::deleting(function ($project) { $project->environments()->delete(); $project->settings()->delete(); + $shared_variables = $project->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting project shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } }); } public function environment_variables() @@ -55,6 +60,7 @@ public function applications() return $this->hasManyThrough(Application::class, Environment::class); } + public function postgresqls() { return $this->hasManyThrough(StandalonePostgresql::class, Environment::class); @@ -91,4 +97,7 @@ public function resource_count() { return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count(); } + 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()); + } } diff --git a/app/Models/Service.php b/app/Models/Service.php index d8950137b0..4c20b71deb 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -653,6 +653,18 @@ public function link() } return null; } + public function failedTaskLink($task_uuid) + { + if (data_get($this, 'environment.project.uuid')) { + return route('project.service.scheduled-tasks', [ + 'project_uuid' => data_get($this, 'environment.project.uuid'), + 'environment_name' => data_get($this, 'environment.name'), + 'application_uuid' => data_get($this, 'uuid'), + 'task_uuid' => $task_uuid + ]); + } + return null; + } public function documentation() { $services = getServiceTemplates(); diff --git a/app/Models/Team.php b/app/Models/Team.php index 29e434a5d2..81206019fd 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -26,6 +26,34 @@ protected static function booted() throw new \Exception('You are not allowed to update this team.'); } }); + + static::deleting(function ($team) { + $keys = $team->privateKeys; + foreach ($keys as $key) { + ray('Deleting key: ' . $key->name); + $key->delete(); + } + $sources = $team->sources(); + foreach ($sources as $source) { + ray('Deleting source: ' . $source->name); + $source->delete(); + } + $tags = Tag::whereTeamId($team->id)->get(); + foreach ($tags as $tag) { + ray('Deleting tag: ' . $tag->name); + $tag->delete(); + } + $shared_variables = $team->environment_variables(); + foreach ($shared_variables as $shared_variable) { + ray('Deleting team shared variable: ' . $shared_variable->name); + $shared_variable->delete(); + } + $s3s = $team->s3s; + foreach ($s3s as $s3) { + ray('Deleting s3: ' . $s3->name); + $s3->delete(); + } + }); } public function routeNotificationForDiscord() diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index 12695497a0..6101ef208a 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -32,6 +32,9 @@ public function send($notifiable, $notification): void case 'App\Notifications\Database\BackupFailed': $topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id'); break; + case 'App\Notifications\ScheduledTask\TaskFailed': + $topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id'); + break; } if (!$telegramToken || !$chatId || !$message) { return; diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index 21dc799f86..d9c524da4f 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -31,7 +31,7 @@ public function toMail(): MailMessage $mail->view('emails.container-restarted', [ 'containerName' => $this->name, 'serverName' => $this->server->name, - 'url' => $this->url , + 'url' => $this->url, ]); return $mail; } diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php new file mode 100644 index 0000000000..f61b1f5731 --- /dev/null +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -0,0 +1,64 @@ +application) { + $this->url = $task->application->failedTaskLink($task->uuid); + } else if ($task->service) { + $this->url = $task->service->failedTaskLink($task->uuid); + } + } + + public function via(object $notifiable): array + { + + return setNotificationChannels($notifiable, 'scheduled_tasks'); + } + + public function toMail(): MailMessage + { + $mail = new MailMessage(); + $mail->subject("Coolify: [ACTION REQUIRED] Scheduled task ({$this->task->name}) failed."); + $mail->view('emails.scheduled-task-failed', [ + 'task' => $this->task, + 'url' => $this->url, + 'output' => $this->output, + ]); + return $mail; + } + + public function toDiscord(): string + { + return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}"; + } + public function toTelegram(): array + { + $message = "Coolify: Scheduled task ({$this->task->name}) failed with output: {$this->output}"; + if ($this->url) { + $buttons[] = [ + "text" => "Open task in Coolify", + "url" => (string) $this->url + ]; + } + return [ + "message" => $message, + ]; + } +} diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 7a960066c9..6453108ebc 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -95,6 +95,9 @@ function currentTeam() function showBoarding(): bool { + if (auth()->user()?->isMember()) { + return false; + } return currentTeam()->show_boarding ?? false; } function refreshSession(?Team $team = null): void @@ -1232,13 +1235,13 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal try { $yaml = Yaml::parse($resource->docker_compose_pr_raw); } catch (\Exception $e) { - throw new \Exception($e->getMessage()); + return; } } else { try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { - throw new \Exception($e->getMessage()); + return; } } $server = $resource->destination->server; diff --git a/config/sentry.php b/config/sentry.php index 0e17d862b5..693c33c3d0 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.284', + 'release' => '4.0.0-beta.285', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 600ee4ea64..e09c4fd6a5 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ boolean('telegram_notifications_scheduled_tasks')->default(true); + $table->boolean('smtp_notifications_scheduled_tasks')->default(false)->after('smtp_notifications_status_changes'); + $table->boolean('discord_notifications_scheduled_tasks')->default(true)->after('discord_notifications_status_changes'); + $table->text('telegram_notifications_scheduled_tasks_thread_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('telegram_notifications_scheduled_tasks'); + $table->dropColumn('smtp_notifications_scheduled_tasks'); + $table->dropColumn('discord_notifications_scheduled_tasks'); + $table->dropColumn('telegram_notifications_scheduled_tasks_thread_id'); + }); + } +}; diff --git a/database/seeders/TestTeamSeeder.php b/database/seeders/TestTeamSeeder.php new file mode 100644 index 0000000000..1d660c7135 --- /dev/null +++ b/database/seeders/TestTeamSeeder.php @@ -0,0 +1,42 @@ +create([ + 'name' => '1 personal, 1 other team, owner, no other members', + 'email' => '1@example.com', + ]); + $team = Team::create([ + 'name' => "1@example.com", + 'personal_team' => false, + 'show_boarding' => true + ]); + $user->teams()->attach($team, ['role' => 'owner']); + + // User has 2 teams, 1 personal, 1 other where it is the owner and 1 other member is in the team + $user = User::factory()->create([ + 'name' => 'owner: 1 personal, 1 other team, owner, 1 other member', + 'email' => '2@example.com', + ]); + $team = Team::create([ + 'name' => "2@example.com", + 'personal_team' => false, + 'show_boarding' => true + ]); + $user->teams()->attach($team, ['role' => 'owner']); + $user = User::factory()->create([ + 'name' => 'member: 1 personal, 1 other team, owner, 1 other member', + 'email' => '3@example.com', + ]); + $team->members()->attach($user, ['role' => 'member']); + } +} diff --git a/resources/css/app.css b/resources/css/app.css index bb05b783b8..cae83b0de5 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -41,7 +41,7 @@ option { } .button { - @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-black hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; + @apply flex items-center justify-center gap-2 px-2 py-1 text-sm text-black normal-case border rounded cursor-pointer bg-neutral-200/50 border-neutral-300 hover:bg-neutral-300 dark:bg-coolgray-200 dark:text-white dark:hover:text-white dark:hover:bg-coolgray-500 dark:border-coolgray-300 hover:text-black disabled:cursor-not-allowed min-w-fit focus:outline-1 dark:disabled:text-neutral-600 disabled:border-none disabled:hover:bg-transparent disabled:bg-transparent disabled:text-neutral-300; } button[isError]:not(:disabled) { diff --git a/resources/views/components/team/navbar.blade.php b/resources/views/components/team/navbar.blade.php index 8bcceeb873..aa88aad51e 100644 --- a/resources/views/components/team/navbar.blade.php +++ b/resources/views/components/team/navbar.blade.php @@ -15,6 +15,12 @@ href="{{ route('team.member.index') }}">
+ @if (isInstanceAdmin()) + + + + @endif
diff --git a/resources/views/emails/scheduled-task-failed.blade.php b/resources/views/emails/scheduled-task-failed.blade.php new file mode 100644 index 0000000000..60e451823f --- /dev/null +++ b/resources/views/emails/scheduled-task-failed.blade.php @@ -0,0 +1,9 @@ + +Scheduled task ({{ $task->name }}) was FAILED with the following error: + +
+{{ $output }}
+
+ +Click [here]({{ $url }}) to view the task. +
diff --git a/resources/views/livewire/notifications/discord.blade.php b/resources/views/livewire/notifications/discord.blade.php index 95fc9aae99..d85f596007 100644 --- a/resources/views/livewire/notifications/discord.blade.php +++ b/resources/views/livewire/notifications/discord.blade.php @@ -32,6 +32,8 @@ label="Application Deployments" /> + @endif diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 9a89057a75..cccd4c26e3 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -111,6 +111,8 @@ label="Application Deployments" /> + @endif diff --git a/resources/views/livewire/notifications/telegram.blade.php b/resources/views/livewire/notifications/telegram.blade.php index 1d8e1231c5..01d8b13290 100644 --- a/resources/views/livewire/notifications/telegram.blade.php +++ b/resources/views/livewire/notifications/telegram.blade.php @@ -60,6 +60,15 @@ helper="If you are using Group chat with Topics, you can specify the topics ID. If empty, General topic will be used." id="team.telegram_notifications_database_backups_message_thread_id" label="Custom Topic ID" /> +
+

Scheduled Tasks Status

+ + +
+ @endif diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index b9a9711246..df7ba526ce 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -38,7 +38,11 @@ @endif @if ($application->build_pack === 'dockercompose') - @if (count($parsedServices) > 0 && !$application->settings->is_raw_compose_deployment_enabled) + + @if ( + !is_null($parsedServices) && + count($parsedServices) > 0 && + !$application->settings->is_raw_compose_deployment_enabled)

Domains

@foreach (data_get($parsedServices, 'services') as $serviceName => $service) @if (!isDatabaseImage(data_get($service, 'image'))) diff --git a/resources/views/livewire/project/new/public-git-repository.blade.php b/resources/views/livewire/project/new/public-git-repository.blade.php index d27e3f259d..62e2e05e6a 100644 --- a/resources/views/livewire/project/new/public-git-repository.blade.php +++ b/resources/views/livewire/project/new/public-git-repository.blade.php @@ -12,7 +12,7 @@ @if (!$branch_found)
-
+
Preselect branch (eg: main):
https://github.com/coollabsio/coolify-examples/tree/main
diff --git a/resources/views/livewire/team/admin-view.blade.php b/resources/views/livewire/team/admin-view.blade.php new file mode 100644 index 0000000000..ae9258603e --- /dev/null +++ b/resources/views/livewire/team/admin-view.blade.php @@ -0,0 +1,29 @@ +
+ +
+ + Search + +

Users

+
+ @forelse ($users as $user) +
+
{{ $user->name }}
+
{{ $user->email }}
+
+
+ + This will delete all resources (application, databases, services, configurations, servers, + private keys, tags, etc.) from Coolify and from the server (if it's reachable). +

+ It is not reversible.

+
Think twice!
+
+
+
+ @empty +
No users found other than the root.
+ @endforelse +
+
diff --git a/resources/views/livewire/team/invite-link.blade.php b/resources/views/livewire/team/invite-link.blade.php index cb67f6b417..3352ae28dd 100644 --- a/resources/views/livewire/team/invite-link.blade.php +++ b/resources/views/livewire/team/invite-link.blade.php @@ -1,5 +1,5 @@
-
+ diff --git a/routes/web.php b/routes/web.php index 8d0651f8f9..feb2dd0eb3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -82,7 +82,7 @@ use App\Livewire\Tags\Index as TagsIndex; use App\Livewire\Tags\Show as TagsShow; - +use App\Livewire\Team\AdminView as TeamAdminView; use App\Livewire\Waitlist\Index as WaitlistIndex; use App\Models\ScheduledDatabaseBackupExecution; use Illuminate\Support\Facades\Storage; @@ -160,6 +160,7 @@ Route::prefix('team')->group(function () { Route::get('/', TeamIndex::class)->name('team.index'); Route::get('/members', TeamMemberIndex::class)->name('team.member.index'); + Route::get('/admin', TeamAdminView::class)->name('team.admin-view'); }); Route::get('/command-center', CommandCenterIndex::class)->name('command-center'); diff --git a/versions.json b/versions.json index caff4c4eae..b389dedd9a 100644 --- a/versions.json +++ b/versions.json @@ -1,7 +1,7 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.284" + "version": "4.0.0-beta.285" }, "sentinel": { "version": "0.0.4"