diff --git a/README.md b/README.md index dac48d127..56edffd31 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,8 @@ Special thanks to our biggest sponsors! ### Special Sponsors -![image](https://github.com/user-attachments/assets/726fb63e-c3b8-4260-b3ac-06780605ec5d) +![image](https://github.com/user-attachments/assets/6022bc9c-8435-4d14-9497-8be230ed8cb1) + * [CCCareers](https://cccareers.org/) - A career development platform connecting coding bootcamp graduates with job opportunities in the tech industry. * [Hetzner](http://htznr.li/CoolifyXHetzner) - A German web hosting company offering affordable dedicated servers, cloud services, and web hosting solutions. @@ -52,7 +53,9 @@ Special thanks to our biggest sponsors! * [SupaGuide](https://supa.guide/?ref=coolify.io) - A comprehensive resource hub offering guides and tutorials for web development using Supabase. * [GoldenVM](https://billing.goldenvm.com/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. * [Tigris](https://tigrisdata.com/?ref=coolify.io) - A fully managed serverless object storage service compatible with Amazon S3 API. Offers high performance, scalability, and built-in search capabilities for efficient data management. -* [Advin](https://coolify.ad.vin/?ref=coolify.io) - A digital advertising agency specializing in programmatic advertising and data-driven marketing strategies. +* [Cloudify.ro](https://cloudify.ro/?ref=coolify.io) - A cloud hosting provider offering scalable infrastructure solutions for businesses of all sizes. +* [Syntaxfm](https://syntax.fm/?ref=coolify.io) - Podcast for web developers. +* [PFGlabs](https://pfglabs.com/?ref=coolify.io) - Build real project with Golang. * [Treive](https://trieve.ai/?ref=coolify.io) - An AI-powered search and discovery platform for enhancing information retrieval in large datasets. * [Blacksmith](https://blacksmith.sh/?ref=coolify.io) - A cloud-native platform for automating infrastructure provisioning and management across multiple cloud providers. * [Brand Dev](https://brand.dev/?ref=coolify.io) - A web development agency specializing in creating custom digital experiences and brand identities. @@ -92,6 +95,9 @@ Special thanks to our biggest sponsors! Michael Mazurczak Formbricks StartupFame +jyc.dev +BitLaunch +Internet Garden Jonas Jaeger JP Evercam diff --git a/app/Console/Commands/Migration.php b/app/Console/Commands/Migration.php new file mode 100644 index 000000000..44c17203b --- /dev/null +++ b/app/Console/Commands/Migration.php @@ -0,0 +1,24 @@ +info('Migration is enabled on this server.'); + $this->call('migrate', ['--force' => true, '--isolated' => true]); + exit(0); + } else { + $this->info('Migration is disabled on this server.'); + exit(0); + } + } +} diff --git a/app/Console/Commands/NotifyDemo.php b/app/Console/Commands/NotifyDemo.php index f0131b7b2..990a03869 100644 --- a/app/Console/Commands/NotifyDemo.php +++ b/app/Console/Commands/NotifyDemo.php @@ -59,9 +59,10 @@ class NotifyDemo extends Command
Channels:
@@ -72,6 +73,6 @@ class NotifyDemo extends Command
In which manner you wish a coolified notification?
- HTML, ['email', 'slack', 'discord', 'telegram']); + HTML, ['email', 'discord', 'telegram', 'slack', 'pushover']); } } diff --git a/app/Console/Commands/Seeder.php b/app/Console/Commands/Seeder.php new file mode 100644 index 000000000..e37b6a9d2 --- /dev/null +++ b/app/Console/Commands/Seeder.php @@ -0,0 +1,24 @@ +info('Seeder is enabled on this server.'); + $this->call('db:seed', ['--class' => 'ProductionSeeder', '--force' => true]); + exit(0); + } else { + $this->info('Seeder is disabled on this server.'); + exit(0); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 19d22ae21..2ed3ee454 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -218,7 +218,7 @@ class Kernel extends ConsoleKernel } } if ($service) { - if (str($service->status())->contains('running') === false) { + if (str($service->status)->contains('running') === false) { continue; } } diff --git a/app/Http/Controllers/Api/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php index 4180cef9a..ad12c83ab 100644 --- a/app/Http/Controllers/Api/ResourcesController.php +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -53,11 +53,7 @@ class ResourcesController extends Controller $resources = $resources->flatten(); $resources = $resources->map(function ($resource) { $payload = $resource->toArray(); - if ($resource->getMorphClass() === \App\Models\Service::class) { - $payload['status'] = $resource->status(); - } else { - $payload['status'] = $resource->status; - } + $payload['status'] = $resource->status; $payload['type'] = $resource->type(); return $payload; diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index f37040bdd..b1deb5321 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -154,11 +154,7 @@ class ServersController extends Controller 'created_at' => $resource->created_at, 'updated_at' => $resource->updated_at, ]; - if ($resource->type() === 'service') { - $payload['status'] = $resource->status(); - } else { - $payload['status'] = $resource->status; - } + $payload['status'] = $resource->status; return $payload; }); @@ -237,11 +233,7 @@ class ServersController extends Controller 'created_at' => $resource->created_at, 'updated_at' => $resource->updated_at, ]; - if ($resource->type() === 'service') { - $payload['status'] = $resource->status(); - } else { - $payload['status'] = $resource->status; - } + $payload['status'] = $resource->status; return $payload; }); diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index e6b7e9854..bcaba7107 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -25,6 +25,8 @@ class ServicesController extends Controller $service->makeHidden([ 'docker_compose_raw', 'docker_compose', + 'value', + 'real_value', ]); } @@ -1070,7 +1072,7 @@ class ServicesController extends Controller if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } - if (str($service->status())->contains('running')) { + if (str($service->status)->contains('running')) { return response()->json(['message' => 'Service is already running.'], 400); } StartService::dispatch($service); @@ -1148,7 +1150,7 @@ class ServicesController extends Controller if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } - if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) { + if (str($service->status)->contains('stopped') || str($service->status)->contains('exited')) { return response()->json(['message' => 'Service is already stopped.'], 400); } StopService::dispatch($service); diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 06aec5e49..6730dceb7 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -32,8 +32,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public Server $server; - public ScheduledDatabaseBackup $backup; - public StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|ServiceDatabase $database; public ?string $container_name = null; @@ -58,10 +56,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue public ?S3Storage $s3 = null; - public function __construct($backup) + public function __construct(public ScheduledDatabaseBackup $backup) { $this->onQueue('high'); - $this->backup = $backup; } public function handle(): void diff --git a/app/Jobs/SendMessageToPushoverJob.php b/app/Jobs/SendMessageToPushoverJob.php new file mode 100644 index 000000000..834a32b07 --- /dev/null +++ b/app/Jobs/SendMessageToPushoverJob.php @@ -0,0 +1,50 @@ +onQueue('high'); + } + + /** + * Execute the job. + */ + public function handle(): void + { + $response = Http::post('https://api.pushover.net/1/messages.json', $this->message->toPayload($this->token, $this->user)); + if ($response->failed()) { + throw new \RuntimeException('Pushover notification failed with ' . $response->status() . ' status code.' . $response->body()); + } + } +} diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php index 85f4fc934..6b0a64ae3 100644 --- a/app/Jobs/SendMessageToTelegramJob.php +++ b/app/Jobs/SendMessageToTelegramJob.php @@ -32,7 +32,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue public array $buttons, public string $token, public string $chatId, - public ?string $topicId = null, + public ?string $threadId = null, ) { $this->onQueue('high'); } @@ -67,8 +67,8 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue 'chat_id' => $this->chatId, 'text' => $this->text, ]; - if ($this->topicId) { - $payload['message_thread_id'] = $this->topicId; + if ($this->threadId) { + $payload['message_thread_id'] = $this->threadId; } $response = Http::post($url, $payload); if ($response->failed()) { diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php index 00c9b6d18..d61c738f4 100644 --- a/app/Jobs/StripeProcessJob.php +++ b/app/Jobs/StripeProcessJob.php @@ -173,8 +173,8 @@ class StripeProcessJob implements ShouldQueue $userId = data_get($data, 'metadata.user_id'); $customerId = data_get($data, 'customer'); $status = data_get($data, 'status'); - $subscriptionId = data_get($data, 'items.data.0.subscription'); - $planId = data_get($data, 'items.data.0.plan.id'); + $subscriptionId = data_get($data, 'items.data.0.subscription') ?? data_get($data, 'id'); + $planId = data_get($data, 'items.data.0.plan.id') ?? data_get($data, 'plan.id'); if (Str::contains($excludedPlans, $planId)) { send_internal_notification('Subscription excluded.'); break; @@ -218,9 +218,18 @@ class StripeProcessJob implements ShouldQueue 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd, ]); if ($status === 'paused' || $status === 'incomplete_expired') { - $subscription->update([ - 'stripe_invoice_paid' => false, - ]); + if ($subscription->stripe_subscription_id === $subscriptionId) { + $subscription->update([ + 'stripe_invoice_paid' => false, + ]); + } + } + if ($status === 'active') { + if ($subscription->stripe_subscription_id === $subscriptionId) { + $subscription->update([ + 'stripe_invoice_paid' => true, + ]); + } } if ($feedback) { $reason = "Cancellation feedback for {$customerId}: '".$feedback."'"; @@ -228,13 +237,24 @@ class StripeProcessJob implements ShouldQueue $reason .= ' with comment: \''.$comment."'"; } } + break; case 'customer.subscription.deleted': - // End subscription $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); - $team = data_get($subscription, 'team'); - $team?->subscriptionEnded(); + $subscriptionId = data_get($data, 'id'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->where('stripe_subscription_id', $subscriptionId)->first(); + if ($subscription) { + $team = data_get($subscription, 'team'); + if ($team) { + $team->subscriptionEnded(); + } else { + send_internal_notification('Subscription deleted but no team found in Coolify for customer: '.$customerId); + throw new \RuntimeException("No team found in Coolify for customer: {$customerId}"); + } + } else { + send_internal_notification('Subscription deleted but no subscription found in Coolify for customer: '.$customerId); + throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}"); + } break; default: throw new \RuntimeException("Unhandled event type: {$type}"); diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index 359db6329..b5f6d2929 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -21,16 +21,28 @@ class Index extends Component public function mount() { - if (! isCloud()) { + if (! isCloud() && ! isDev()) { return redirect()->route('dashboard'); } - - if (Auth::id() !== 0) { + if (Auth::id() !== 0 && ! session('impersonating')) { return redirect()->route('dashboard'); } $this->getSubscribers(); } + public function back() + { + if (session('impersonating')) { + session()->forget('impersonating'); + $user = User::find(0); + $team_to_switch_to = $user->teams->first(); + Auth::login($user); + refreshSession($team_to_switch_to); + + return redirect(request()->header('Referer')); + } + } + public function submitSearch() { if ($this->search !== '') { @@ -52,9 +64,10 @@ class Index extends Component if (Auth::id() !== 0) { return redirect()->route('dashboard'); } + session(['impersonating' => true]); $user = User::find($user_id); $team_to_switch_to = $user->teams->first(); - Cache::forget("team:{$user->id}"); + // Cache::forget("team:{$user->id}"); Auth::login($user); refreshSession($team_to_switch_to); diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index dc2a95e84..eac48cf77 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -14,8 +14,10 @@ class Email extends Component { protected $listeners = ['refresh' => '$refresh']; + #[Locked] public Team $team; + #[Locked] public EmailNotificationSettings $settings; #[Locked] diff --git a/app/Livewire/Notifications/Pushover.php b/app/Livewire/Notifications/Pushover.php new file mode 100644 index 000000000..f1e4c464d --- /dev/null +++ b/app/Livewire/Notifications/Pushover.php @@ -0,0 +1,184 @@ + '$refresh']; + + #[Locked] + public Team $team; + + #[Locked] + public PushoverNotificationSettings $settings; + + #[Validate(['boolean'])] + public bool $pushoverEnabled = false; + + #[Validate(['nullable', 'string'])] + public ?string $pushoverUserKey = null; + + #[Validate(['nullable', 'string'])] + public ?string $pushoverApiToken = null; + + #[Validate(['boolean'])] + public bool $deploymentSuccessPushoverNotifications = false; + + #[Validate(['boolean'])] + public bool $deploymentFailurePushoverNotifications = true; + + #[Validate(['boolean'])] + public bool $statusChangePushoverNotifications = false; + + #[Validate(['boolean'])] + public bool $backupSuccessPushoverNotifications = false; + + #[Validate(['boolean'])] + public bool $backupFailurePushoverNotifications = true; + + #[Validate(['boolean'])] + public bool $scheduledTaskSuccessPushoverNotifications = false; + + #[Validate(['boolean'])] + public bool $scheduledTaskFailurePushoverNotifications = true; + + #[Validate(['boolean'])] + public bool $dockerCleanupSuccessPushoverNotifications = false; + + #[Validate(['boolean'])] + public bool $dockerCleanupFailurePushoverNotifications = true; + + #[Validate(['boolean'])] + public bool $serverDiskUsagePushoverNotifications = true; + + #[Validate(['boolean'])] + public bool $serverReachablePushoverNotifications = false; + + #[Validate(['boolean'])] + public bool $serverUnreachablePushoverNotifications = true; + + public function mount() + { + try { + $this->team = auth()->user()->currentTeam(); + $this->settings = $this->team->pushoverNotificationSettings; + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->settings->pushover_enabled = $this->pushoverEnabled; + $this->settings->pushover_user_key = $this->pushoverUserKey; + $this->settings->pushover_api_token = $this->pushoverApiToken; + + $this->settings->deployment_success_pushover_notifications = $this->deploymentSuccessPushoverNotifications; + $this->settings->deployment_failure_pushover_notifications = $this->deploymentFailurePushoverNotifications; + $this->settings->status_change_pushover_notifications = $this->statusChangePushoverNotifications; + $this->settings->backup_success_pushover_notifications = $this->backupSuccessPushoverNotifications; + $this->settings->backup_failure_pushover_notifications = $this->backupFailurePushoverNotifications; + $this->settings->scheduled_task_success_pushover_notifications = $this->scheduledTaskSuccessPushoverNotifications; + $this->settings->scheduled_task_failure_pushover_notifications = $this->scheduledTaskFailurePushoverNotifications; + $this->settings->docker_cleanup_success_pushover_notifications = $this->dockerCleanupSuccessPushoverNotifications; + $this->settings->docker_cleanup_failure_pushover_notifications = $this->dockerCleanupFailurePushoverNotifications; + $this->settings->server_disk_usage_pushover_notifications = $this->serverDiskUsagePushoverNotifications; + $this->settings->server_reachable_pushover_notifications = $this->serverReachablePushoverNotifications; + $this->settings->server_unreachable_pushover_notifications = $this->serverUnreachablePushoverNotifications; + + $this->settings->save(); + refreshSession(); + } else { + $this->pushoverEnabled = $this->settings->pushover_enabled; + $this->pushoverUserKey = $this->settings->pushover_user_key; + $this->pushoverApiToken = $this->settings->pushover_api_token; + + $this->deploymentSuccessPushoverNotifications = $this->settings->deployment_success_pushover_notifications; + $this->deploymentFailurePushoverNotifications = $this->settings->deployment_failure_pushover_notifications; + $this->statusChangePushoverNotifications = $this->settings->status_change_pushover_notifications; + $this->backupSuccessPushoverNotifications = $this->settings->backup_success_pushover_notifications; + $this->backupFailurePushoverNotifications = $this->settings->backup_failure_pushover_notifications; + $this->scheduledTaskSuccessPushoverNotifications = $this->settings->scheduled_task_success_pushover_notifications; + $this->scheduledTaskFailurePushoverNotifications = $this->settings->scheduled_task_failure_pushover_notifications; + $this->dockerCleanupSuccessPushoverNotifications = $this->settings->docker_cleanup_success_pushover_notifications; + $this->dockerCleanupFailurePushoverNotifications = $this->settings->docker_cleanup_failure_pushover_notifications; + $this->serverDiskUsagePushoverNotifications = $this->settings->server_disk_usage_pushover_notifications; + $this->serverReachablePushoverNotifications = $this->settings->server_reachable_pushover_notifications; + $this->serverUnreachablePushoverNotifications = $this->settings->server_unreachable_pushover_notifications; + } + } + + public function instantSavePushoverEnabled() + { + try { + $this->validate([ + 'pushoverUserKey' => 'required', + 'pushoverApiToken' => 'required', + ], [ + 'pushoverUserKey.required' => 'Pushover User Key is required.', + 'pushoverApiToken.required' => 'Pushover API Token is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->pushoverEnabled = false; + + return handleError($e, $this); + } finally { + $this->dispatch('refresh'); + } + } + + public function instantSave() + { + try { + $this->syncData(true); + } catch (\Throwable $e) { + return handleError($e, $this); + } finally { + $this->dispatch('refresh'); + } + } + + public function submit() + { + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->saveModel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function saveModel() + { + $this->syncData(true); + refreshSession(); + $this->dispatch('success', 'Settings saved.'); + } + + public function sendTestNotification() + { + try { + $this->team->notify(new Test(channel: 'pushover')); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.notifications.pushover'); + } +} diff --git a/app/Livewire/Notifications/Slack.php b/app/Livewire/Notifications/Slack.php index 97464fa1c..f47ad8a97 100644 --- a/app/Livewire/Notifications/Slack.php +++ b/app/Livewire/Notifications/Slack.php @@ -5,13 +5,18 @@ namespace App\Livewire\Notifications; use App\Models\SlackNotificationSettings; use App\Models\Team; use App\Notifications\Test; +use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; use Livewire\Component; class Slack extends Component { + protected $listeners = ['refresh' => '$refresh']; + + #[Locked] public Team $team; + #[Locked] public SlackNotificationSettings $settings; #[Validate(['boolean'])] @@ -121,6 +126,8 @@ class Slack extends Component $this->slackEnabled = false; return handleError($e, $this); + } finally { + $this->dispatch('refresh'); } } @@ -130,6 +137,8 @@ class Slack extends Component $this->syncData(true); } catch (\Throwable $e) { return handleError($e, $this); + } finally { + $this->dispatch('refresh'); } } diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index de2fa9cdc..01afb4ac6 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -5,13 +5,18 @@ namespace App\Livewire\Notifications; use App\Models\Team; use App\Models\TelegramNotificationSettings; use App\Notifications\Test; +use Livewire\Attributes\Locked; use Livewire\Attributes\Validate; use Livewire\Component; class Telegram extends Component { + protected $listeners = ['refresh' => '$refresh']; + + #[Locked] public Team $team; + #[Locked] public TelegramNotificationSettings $settings; #[Validate(['boolean'])] @@ -60,40 +65,40 @@ class Telegram extends Component public bool $serverUnreachableTelegramNotifications = true; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsDeploymentSuccessTopicId = null; + public ?string $telegramNotificationsDeploymentSuccessThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsDeploymentFailureTopicId = null; + public ?string $telegramNotificationsDeploymentFailureThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsStatusChangeTopicId = null; + public ?string $telegramNotificationsStatusChangeThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsBackupSuccessTopicId = null; + public ?string $telegramNotificationsBackupSuccessThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsBackupFailureTopicId = null; + public ?string $telegramNotificationsBackupFailureThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsScheduledTaskSuccessTopicId = null; + public ?string $telegramNotificationsScheduledTaskSuccessThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsScheduledTaskFailureTopicId = null; + public ?string $telegramNotificationsScheduledTaskFailureThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsDockerCleanupSuccessTopicId = null; + public ?string $telegramNotificationsDockerCleanupSuccessThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsDockerCleanupFailureTopicId = null; + public ?string $telegramNotificationsDockerCleanupFailureThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsServerDiskUsageTopicId = null; + public ?string $telegramNotificationsServerDiskUsageThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsServerReachableTopicId = null; + public ?string $telegramNotificationsServerReachableThreadId = null; #[Validate(['nullable', 'string'])] - public ?string $telegramNotificationsServerUnreachableTopicId = null; + public ?string $telegramNotificationsServerUnreachableThreadId = null; public function mount() { @@ -127,21 +132,20 @@ class Telegram extends Component $this->settings->server_reachable_telegram_notifications = $this->serverReachableTelegramNotifications; $this->settings->server_unreachable_telegram_notifications = $this->serverUnreachableTelegramNotifications; - $this->settings->telegram_notifications_deployment_success_topic_id = $this->telegramNotificationsDeploymentSuccessTopicId; - $this->settings->telegram_notifications_deployment_failure_topic_id = $this->telegramNotificationsDeploymentFailureTopicId; - $this->settings->telegram_notifications_status_change_topic_id = $this->telegramNotificationsStatusChangeTopicId; - $this->settings->telegram_notifications_backup_success_topic_id = $this->telegramNotificationsBackupSuccessTopicId; - $this->settings->telegram_notifications_backup_failure_topic_id = $this->telegramNotificationsBackupFailureTopicId; - $this->settings->telegram_notifications_scheduled_task_success_topic_id = $this->telegramNotificationsScheduledTaskSuccessTopicId; - $this->settings->telegram_notifications_scheduled_task_failure_topic_id = $this->telegramNotificationsScheduledTaskFailureTopicId; - $this->settings->telegram_notifications_docker_cleanup_success_topic_id = $this->telegramNotificationsDockerCleanupSuccessTopicId; - $this->settings->telegram_notifications_docker_cleanup_failure_topic_id = $this->telegramNotificationsDockerCleanupFailureTopicId; - $this->settings->telegram_notifications_server_disk_usage_topic_id = $this->telegramNotificationsServerDiskUsageTopicId; - $this->settings->telegram_notifications_server_reachable_topic_id = $this->telegramNotificationsServerReachableTopicId; - $this->settings->telegram_notifications_server_unreachable_topic_id = $this->telegramNotificationsServerUnreachableTopicId; + $this->settings->telegram_notifications_deployment_success_thread_id = $this->telegramNotificationsDeploymentSuccessThreadId; + $this->settings->telegram_notifications_deployment_failure_thread_id = $this->telegramNotificationsDeploymentFailureThreadId; + $this->settings->telegram_notifications_status_change_thread_id = $this->telegramNotificationsStatusChangeThreadId; + $this->settings->telegram_notifications_backup_success_thread_id = $this->telegramNotificationsBackupSuccessThreadId; + $this->settings->telegram_notifications_backup_failure_thread_id = $this->telegramNotificationsBackupFailureThreadId; + $this->settings->telegram_notifications_scheduled_task_success_thread_id = $this->telegramNotificationsScheduledTaskSuccessThreadId; + $this->settings->telegram_notifications_scheduled_task_failure_thread_id = $this->telegramNotificationsScheduledTaskFailureThreadId; + $this->settings->telegram_notifications_docker_cleanup_success_thread_id = $this->telegramNotificationsDockerCleanupSuccessThreadId; + $this->settings->telegram_notifications_docker_cleanup_failure_thread_id = $this->telegramNotificationsDockerCleanupFailureThreadId; + $this->settings->telegram_notifications_server_disk_usage_thread_id = $this->telegramNotificationsServerDiskUsageThreadId; + $this->settings->telegram_notifications_server_reachable_thread_id = $this->telegramNotificationsServerReachableThreadId; + $this->settings->telegram_notifications_server_unreachable_thread_id = $this->telegramNotificationsServerUnreachableThreadId; $this->settings->save(); - refreshSession(); } else { $this->telegramEnabled = $this->settings->telegram_enabled; $this->telegramToken = $this->settings->telegram_token; @@ -160,18 +164,18 @@ class Telegram extends Component $this->serverReachableTelegramNotifications = $this->settings->server_reachable_telegram_notifications; $this->serverUnreachableTelegramNotifications = $this->settings->server_unreachable_telegram_notifications; - $this->telegramNotificationsDeploymentSuccessTopicId = $this->settings->telegram_notifications_deployment_success_topic_id; - $this->telegramNotificationsDeploymentFailureTopicId = $this->settings->telegram_notifications_deployment_failure_topic_id; - $this->telegramNotificationsStatusChangeTopicId = $this->settings->telegram_notifications_status_change_topic_id; - $this->telegramNotificationsBackupSuccessTopicId = $this->settings->telegram_notifications_backup_success_topic_id; - $this->telegramNotificationsBackupFailureTopicId = $this->settings->telegram_notifications_backup_failure_topic_id; - $this->telegramNotificationsScheduledTaskSuccessTopicId = $this->settings->telegram_notifications_scheduled_task_success_topic_id; - $this->telegramNotificationsScheduledTaskFailureTopicId = $this->settings->telegram_notifications_scheduled_task_failure_topic_id; - $this->telegramNotificationsDockerCleanupSuccessTopicId = $this->settings->telegram_notifications_docker_cleanup_success_topic_id; - $this->telegramNotificationsDockerCleanupFailureTopicId = $this->settings->telegram_notifications_docker_cleanup_failure_topic_id; - $this->telegramNotificationsServerDiskUsageTopicId = $this->settings->telegram_notifications_server_disk_usage_topic_id; - $this->telegramNotificationsServerReachableTopicId = $this->settings->telegram_notifications_server_reachable_topic_id; - $this->telegramNotificationsServerUnreachableTopicId = $this->settings->telegram_notifications_server_unreachable_topic_id; + $this->telegramNotificationsDeploymentSuccessThreadId = $this->settings->telegram_notifications_deployment_success_thread_id; + $this->telegramNotificationsDeploymentFailureThreadId = $this->settings->telegram_notifications_deployment_failure_thread_id; + $this->telegramNotificationsStatusChangeThreadId = $this->settings->telegram_notifications_status_change_thread_id; + $this->telegramNotificationsBackupSuccessThreadId = $this->settings->telegram_notifications_backup_success_thread_id; + $this->telegramNotificationsBackupFailureThreadId = $this->settings->telegram_notifications_backup_failure_thread_id; + $this->telegramNotificationsScheduledTaskSuccessThreadId = $this->settings->telegram_notifications_scheduled_task_success_thread_id; + $this->telegramNotificationsScheduledTaskFailureThreadId = $this->settings->telegram_notifications_scheduled_task_failure_thread_id; + $this->telegramNotificationsDockerCleanupSuccessThreadId = $this->settings->telegram_notifications_docker_cleanup_success_thread_id; + $this->telegramNotificationsDockerCleanupFailureThreadId = $this->settings->telegram_notifications_docker_cleanup_failure_thread_id; + $this->telegramNotificationsServerDiskUsageThreadId = $this->settings->telegram_notifications_server_disk_usage_thread_id; + $this->telegramNotificationsServerReachableThreadId = $this->settings->telegram_notifications_server_reachable_thread_id; + $this->telegramNotificationsServerUnreachableThreadId = $this->settings->telegram_notifications_server_unreachable_thread_id; } } @@ -181,6 +185,8 @@ class Telegram extends Component $this->syncData(true); } catch (\Throwable $e) { return handleError($e, $this); + } finally { + $this->dispatch('refresh'); } } @@ -210,6 +216,8 @@ class Telegram extends Component $this->telegramEnabled = false; return handleError($e, $this); + } finally { + $this->dispatch('refresh'); } } diff --git a/app/Livewire/Project/Application/Configuration.php b/app/Livewire/Project/Application/Configuration.php index 5261a0800..bf2811075 100644 --- a/app/Livewire/Project/Application/Configuration.php +++ b/app/Livewire/Project/Application/Configuration.php @@ -8,14 +8,21 @@ use Livewire\Component; class Configuration extends Component { + public $currentRoute; + public Application $application; + public $project; + + public $environment; + public $servers; protected $listeners = ['buildPackUpdated' => '$refresh']; public function mount() { + $this->currentRoute = request()->route()->getName(); $project = currentTeam() ->projects() ->select('id', 'uuid', 'team_id') @@ -30,6 +37,8 @@ class Configuration extends Component ->where('uuid', request()->route('application_uuid')) ->firstOrFail(); + $this->project = $project; + $this->environment = $environment; $this->application = $application; if ($application->destination && $application->destination->server) { $mainServer = $application->destination->server; diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index ff29b74e9..d1cc3476c 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -327,7 +327,7 @@ class General extends Component } } - public function set_redirect() + public function setRedirect() { try { $has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count(); @@ -360,10 +360,10 @@ class General extends Component if ($warning) { $this->dispatch('warning', __('warning.sslipdomain')); } - $this->resetDefaultLabels(); + // $this->resetDefaultLabels(); if ($this->application->isDirty('redirect')) { - $this->set_redirect(); + $this->setRedirect(); } $this->checkFqdns(); diff --git a/app/Livewire/Project/Database/BackupNow.php b/app/Livewire/Project/Database/BackupNow.php index 9c9c175e2..3cd360562 100644 --- a/app/Livewire/Project/Database/BackupNow.php +++ b/app/Livewire/Project/Database/BackupNow.php @@ -9,11 +9,9 @@ class BackupNow extends Component { public $backup; - public function backup_now() + public function backupNow() { - dispatch(new DatabaseBackupJob( - backup: $this->backup - )); + DatabaseBackupJob::dispatch($this->backup); $this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); } } diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php index 283496887..0c2ea802a 100644 --- a/app/Livewire/Project/Resource/Index.php +++ b/app/Livewire/Project/Resource/Index.php @@ -46,125 +46,84 @@ class Index extends Component return redirect()->route('dashboard'); } $this->project = $project; - $this->environment = $environment; - $this->applications = $this->environment->applications->load(['tags']); + $this->environment = $environment->loadCount([ + 'applications', + 'redis', + 'postgresqls', + 'mysqls', + 'keydbs', + 'dragonflies', + 'clickhouses', + 'mariadbs', + 'mongodbs', + 'services', + ]); + + // Eager load all relationships for applications including nested ones + $this->applications = $this->environment->applications()->with([ + 'tags', + 'additional_servers.settings', + 'additional_networks', + 'destination.server.settings', + 'settings', + ])->get()->sortBy('name'); $this->applications = $this->applications->map(function ($application) { - if (data_get($application, 'environment.project.uuid')) { - $application->hrefLink = route('project.application.configuration', [ - 'project_uuid' => data_get($application, 'environment.project.uuid'), - 'environment_name' => data_get($application, 'environment.name'), - 'application_uuid' => data_get($application, 'uuid'), - ]); - } + $application->hrefLink = route('project.application.configuration', [ + 'project_uuid' => $this->project->uuid, + 'application_uuid' => $application->uuid, + 'environment_name' => $this->environment->name, + ]); return $application; }); - $this->postgresqls = $this->environment->postgresqls->load(['tags'])->sortBy('name'); - $this->postgresqls = $this->postgresqls->map(function ($postgresql) { - if (data_get($postgresql, 'environment.project.uuid')) { - $postgresql->hrefLink = route('project.database.configuration', [ - 'project_uuid' => data_get($postgresql, 'environment.project.uuid'), - 'environment_name' => data_get($postgresql, 'environment.name'), - 'database_uuid' => data_get($postgresql, 'uuid'), - ]); - } - return $postgresql; - }); - $this->redis = $this->environment->redis->load(['tags'])->sortBy('name'); - $this->redis = $this->redis->map(function ($redis) { - if (data_get($redis, 'environment.project.uuid')) { - $redis->hrefLink = route('project.database.configuration', [ - 'project_uuid' => data_get($redis, 'environment.project.uuid'), - 'environment_name' => data_get($redis, 'environment.name'), - 'database_uuid' => data_get($redis, 'uuid'), - ]); - } + // Load all database resources in a single query per type + $databaseTypes = [ + 'postgresqls' => 'postgresqls', + 'redis' => 'redis', + 'mongodbs' => 'mongodbs', + 'mysqls' => 'mysqls', + 'mariadbs' => 'mariadbs', + 'keydbs' => 'keydbs', + 'dragonflies' => 'dragonflies', + 'clickhouses' => 'clickhouses', + ]; - return $redis; - }); - $this->mongodbs = $this->environment->mongodbs->load(['tags'])->sortBy('name'); - $this->mongodbs = $this->mongodbs->map(function ($mongodb) { - if (data_get($mongodb, 'environment.project.uuid')) { - $mongodb->hrefLink = route('project.database.configuration', [ - 'project_uuid' => data_get($mongodb, 'environment.project.uuid'), - 'environment_name' => data_get($mongodb, 'environment.name'), - 'database_uuid' => data_get($mongodb, 'uuid'), - ]); - } + // Load all server-related data first to prevent duplicate queries + $serverData = $this->environment->applications() + ->with(['destination.server.settings']) + ->get() + ->pluck('destination.server') + ->filter() + ->unique('id'); - return $mongodb; - }); - $this->mysqls = $this->environment->mysqls->load(['tags'])->sortBy('name'); - $this->mysqls = $this->mysqls->map(function ($mysql) { - if (data_get($mysql, 'environment.project.uuid')) { - $mysql->hrefLink = route('project.database.configuration', [ - 'project_uuid' => data_get($mysql, 'environment.project.uuid'), - 'environment_name' => data_get($mysql, 'environment.name'), - 'database_uuid' => data_get($mysql, 'uuid'), + foreach ($databaseTypes as $property => $relation) { + $this->{$property} = $this->environment->{$relation}()->with([ + 'tags', + 'destination.server.settings', + ])->get()->sortBy('name'); + $this->{$property} = $this->{$property}->map(function ($db) { + $db->hrefLink = route('project.database.configuration', [ + 'project_uuid' => $this->project->uuid, + 'database_uuid' => $db->uuid, + 'environment_name' => $this->environment->name, ]); - } - return $mysql; - }); - $this->mariadbs = $this->environment->mariadbs->load(['tags'])->sortBy('name'); - $this->mariadbs = $this->mariadbs->map(function ($mariadb) { - if (data_get($mariadb, 'environment.project.uuid')) { - $mariadb->hrefLink = route('project.database.configuration', [ - 'project_uuid' => data_get($mariadb, 'environment.project.uuid'), - 'environment_name' => data_get($mariadb, 'environment.name'), - 'database_uuid' => data_get($mariadb, 'uuid'), - ]); - } + return $db; + }); + } - return $mariadb; - }); - $this->keydbs = $this->environment->keydbs->load(['tags'])->sortBy('name'); - $this->keydbs = $this->keydbs->map(function ($keydb) { - if (data_get($keydb, 'environment.project.uuid')) { - $keydb->hrefLink = route('project.database.configuration', [ - 'project_uuid' => data_get($keydb, 'environment.project.uuid'), - 'environment_name' => data_get($keydb, 'environment.name'), - 'database_uuid' => data_get($keydb, 'uuid'), - ]); - } - - return $keydb; - }); - $this->dragonflies = $this->environment->dragonflies->load(['tags'])->sortBy('name'); - $this->dragonflies = $this->dragonflies->map(function ($dragonfly) { - if (data_get($dragonfly, 'environment.project.uuid')) { - $dragonfly->hrefLink = route('project.database.configuration', [ - 'project_uuid' => data_get($dragonfly, 'environment.project.uuid'), - 'environment_name' => data_get($dragonfly, 'environment.name'), - 'database_uuid' => data_get($dragonfly, 'uuid'), - ]); - } - - return $dragonfly; - }); - $this->clickhouses = $this->environment->clickhouses->load(['tags'])->sortBy('name'); - $this->clickhouses = $this->clickhouses->map(function ($clickhouse) { - if (data_get($clickhouse, 'environment.project.uuid')) { - $clickhouse->hrefLink = route('project.database.configuration', [ - 'project_uuid' => data_get($clickhouse, 'environment.project.uuid'), - 'environment_name' => data_get($clickhouse, 'environment.name'), - 'database_uuid' => data_get($clickhouse, 'uuid'), - ]); - } - - return $clickhouse; - }); - $this->services = $this->environment->services->load(['tags'])->sortBy('name'); + // Load services with their tags and server + $this->services = $this->environment->services()->with([ + 'tags', + 'destination.server.settings', + ])->get()->sortBy('name'); $this->services = $this->services->map(function ($service) { - if (data_get($service, 'environment.project.uuid')) { - $service->hrefLink = route('project.service.configuration', [ - 'project_uuid' => data_get($service, 'environment.project.uuid'), - 'environment_name' => data_get($service, 'environment.name'), - 'service_uuid' => data_get($service, 'uuid'), - ]); - $service->status = $service->status(); - } + $service->hrefLink = route('project.service.configuration', [ + 'project_uuid' => $this->project->uuid, + 'service_uuid' => $service->uuid, + 'environment_name' => $this->environment->name, + ]); return $service; }); diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index ee43dc911..22fc1c0d6 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -27,7 +27,7 @@ class Navbar extends Component public function mount() { - if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { + if (str($this->service->status)->contains('running') && is_null($this->service->config_hash)) { $this->service->isConfigurationChanged(true); $this->dispatch('configurationChanged'); } diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 6294d97c6..2bccde0e9 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -5,7 +5,6 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable; use App\Models\EnvironmentVariable as ModelsEnvironmentVariable; use App\Models\SharedEnvironmentVariable; use Livewire\Component; -use Visus\Cuid2\Cuid2; class Show extends Component { @@ -13,8 +12,6 @@ class Show extends Component public ModelsEnvironmentVariable|SharedEnvironmentVariable $env; - public ?string $modalId = null; - public bool $isDisabled = false; public bool $isLocked = false; @@ -61,7 +58,6 @@ class Show extends Component if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) { $this->isSharedVariable = true; } - $this->modalId = new Cuid2; $this->parameters = get_route_parameters(); $this->checkEnvs(); } diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index e67df6aa9..de7bb3c05 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -42,9 +42,11 @@ class ResourceOperations extends Component $uuid = (string) new Cuid2; $server = $new_destination->server; if ($this->resource->getMorphClass() === \App\Models\Application::class) { + $name = 'clone-of-'.str($this->resource->name)->limit(20).'-'.$uuid; + $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, - 'name' => $this->resource->name.'-clone-'.$uuid, + 'name' => $name, 'fqdn' => generateFqdn($server, $uuid), 'status' => 'exited', 'destination_id' => $new_destination->id, @@ -64,8 +66,12 @@ class ResourceOperations extends Component } $persistentVolumes = $this->resource->persistentStorages()->get(); foreach ($persistentVolumes as $volume) { + $volumeName = str($volume->name)->replace($this->resource->uuid, $new_resource->uuid)->value(); + if ($volumeName === $volume->name) { + $volumeName = $new_resource->uuid.'-'.str($volume->name)->afterLast('-'); + } $newPersistentVolume = $volume->replicate()->fill([ - 'name' => $new_resource->uuid.'-'.str($volume->name)->afterLast('-'), + 'name' => $volumeName, 'resource_id' => $new_resource->id, ]); $newPersistentVolume->save(); diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php index 17b3b89a3..e23f94a73 100644 --- a/app/Livewire/SettingsOauth.php +++ b/app/Livewire/SettingsOauth.php @@ -17,6 +17,7 @@ class SettingsOauth extends Component $carry["oauth_settings_map.$setting->provider.client_secret"] = 'nullable'; $carry["oauth_settings_map.$setting->provider.redirect_uri"] = 'nullable'; $carry["oauth_settings_map.$setting->provider.tenant"] = 'nullable'; + $carry["oauth_settings_map.$setting->provider.base_url"] = 'nullable'; return $carry; }, []); @@ -34,16 +35,30 @@ class SettingsOauth extends Component }, []); } - private function updateOauthSettings() + private function updateOauthSettings(?string $provider = null) { - foreach (array_values($this->oauth_settings_map) as &$setting) { - $setting->save(); + if ($provider) { + $oauth = $this->oauth_settings_map[$provider]; + if (! $oauth->couldBeEnabled()) { + $oauth->update(['enabled' => false]); + throw new \Exception('OAuth settings are not complete for '.$oauth->provider.'.
Please fill in all required fields.'); + } + $oauth->save(); + $this->dispatch('success', 'OAuth settings for '.$oauth->provider.' updated successfully!'); + } else { + foreach (array_values($this->oauth_settings_map) as &$setting) { + $setting->save(); + } } } - public function instantSave() + public function instantSave(string $provider) { - $this->updateOauthSettings(); + try { + $this->updateOauthSettings($provider); + } catch (\Exception $e) { + return handleError($e, $this); + } } public function submit() diff --git a/app/Models/Application.php b/app/Models/Application.php index 82ad0c2d1..bfb2a1041 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -115,6 +115,12 @@ class Application extends BaseModel protected static function booted() { + static::addGlobalScope('withRelations', function ($builder) { + $builder->withCount([ + 'additional_servers', + 'additional_networks', + ]); + }); static::saving(function ($application) { $payload = []; if ($application->isDirty('fqdn')) { @@ -551,20 +557,21 @@ class Application extends BaseModel { return Attribute::make( get: function () { - if ($this->additional_servers->count() === 0) { - return $this->destination->server->isFunctional(); - } else { - $additional_servers_status = $this->additional_servers->pluck('pivot.status'); - $main_server_status = $this->destination->server->isFunctional(); - foreach ($additional_servers_status as $status) { - $server_status = str($status)->before(':')->value(); - if ($server_status !== 'running') { - return false; - } - } - - return $main_server_status; + if (! $this->relationLoaded('additional_servers') || $this->additional_servers->count() === 0) { + return $this->destination?->server?->isFunctional() ?? false; } + + $additional_servers_status = $this->additional_servers->pluck('pivot.status'); + $main_server_status = $this->destination?->server?->isFunctional() ?? false; + + foreach ($additional_servers_status as $status) { + $server_status = str($status)->before(':')->value(); + if ($server_status !== 'running') { + return false; + } + } + + return $main_server_status; } ); } @@ -1331,7 +1338,9 @@ class Application extends BaseModel $currentPath = ''; foreach ($parts as $part) { $currentPath .= ($currentPath ? '/' : '').$part; - $paths->push($currentPath); + if (str($currentPath)->isNotEmpty()) { + $paths->push($currentPath); + } } return $paths; @@ -1341,7 +1350,7 @@ class Application extends BaseModel "mkdir -p /tmp/{$uuid}", "cd /tmp/{$uuid}", $cloneCommand, - 'git sparse-checkout init --cone', + 'git sparse-checkout init', "git sparse-checkout set {$fileList->implode(' ')}", 'git read-tree -mu HEAD', "cat .$workdir$composeFile", diff --git a/app/Models/OauthSetting.php b/app/Models/OauthSetting.php index c17c318f1..3d82e89f2 100644 --- a/app/Models/OauthSetting.php +++ b/app/Models/OauthSetting.php @@ -11,6 +11,8 @@ class OauthSetting extends Model { use HasFactory; + protected $fillable = ['provider', 'client_id', 'client_secret', 'redirect_uri', 'tenant', 'base_url', 'enabled']; + protected function clientSecret(): Attribute { return Attribute::make( @@ -18,4 +20,16 @@ class OauthSetting extends Model set: fn (?string $value) => empty($value) ? null : Crypt::encryptString($value), ); } + + public function couldBeEnabled(): bool + { + switch ($this->provider) { + case 'azure': + return filled($this->client_id) && filled($this->client_secret) && filled($this->redirect_uri) && filled($this->tenant); + case 'authentik': + return filled($this->client_id) && filled($this->client_secret) && filled($this->redirect_uri) && filled($this->base_url); + default: + return filled($this->client_id) && filled($this->client_secret) && filled($this->redirect_uri); + } + } } diff --git a/app/Models/PushoverNotificationSettings.php b/app/Models/PushoverNotificationSettings.php new file mode 100644 index 000000000..e3191dcc3 --- /dev/null +++ b/app/Models/PushoverNotificationSettings.php @@ -0,0 +1,61 @@ + 'boolean', + 'pushover_user_key' => 'encrypted', + 'pushover_api_token' => 'encrypted', + + 'deployment_success_pushover_notifications' => 'boolean', + 'deployment_failure_pushover_notifications' => 'boolean', + 'status_change_pushover_notifications' => 'boolean', + 'backup_success_pushover_notifications' => 'boolean', + 'backup_failure_pushover_notifications' => 'boolean', + 'scheduled_task_success_pushover_notifications' => 'boolean', + 'scheduled_task_failure_pushover_notifications' => 'boolean', + 'docker_cleanup_pushover_notifications' => 'boolean', + 'server_disk_usage_pushover_notifications' => 'boolean', + 'server_reachable_pushover_notifications' => 'boolean', + 'server_unreachable_pushover_notifications' => 'boolean', + ]; + + public function team() + { + return $this->belongsTo(Team::class); + } + + public function isEnabled() + { + return $this->pushover_enabled; + } +} diff --git a/app/Models/Service.php b/app/Models/Service.php index 117677d53..5a2690490 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -46,7 +46,7 @@ class Service extends BaseModel protected $guarded = []; - protected $appends = ['server_status']; + protected $appends = ['server_status', 'status']; protected static function booted() { @@ -105,12 +105,12 @@ class Service extends BaseModel public function isRunning() { - return (bool) str($this->status())->contains('running'); + return (bool) str($this->status)->contains('running'); } public function isExited() { - return (bool) str($this->status())->contains('exited'); + return (bool) str($this->status)->contains('exited'); } public function type() @@ -213,7 +213,7 @@ class Service extends BaseModel instant_remote_process(["docker network rm {$uuid}"], $server, false); } - public function status() + public function getStatusAttribute() { $applications = $this->applications; $databases = $this->databases; diff --git a/app/Models/Team.php b/app/Models/Team.php index 07424a55f..e55cb0d19 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -4,6 +4,7 @@ namespace App\Models; use App\Notifications\Channels\SendsDiscord; use App\Notifications\Channels\SendsEmail; +use App\Notifications\Channels\SendsPushover; use App\Notifications\Channels\SendsSlack; use App\Traits\HasNotificationSettings; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -32,7 +33,7 @@ use OpenApi\Attributes as OA; ] )] -class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack +class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, SendsSlack { use HasNotificationSettings, Notifiable; @@ -49,6 +50,7 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack $team->discordNotificationSettings()->create(); $team->slackNotificationSettings()->create(); $team->telegramNotificationSettings()->create(); + $team->pushoverNotificationSettings()->create(); }); static::saving(function ($team) { @@ -155,6 +157,14 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack return data_get($this, 'slack_webhook_url', null); } + public function routeNotificationForPushover() + { + return [ + 'user' => data_get($this, 'pushover_user_key', null), + 'token' => data_get($this, 'pushover_api_token', null), + ]; + } + public function getRecipients($notification) { $recipients = data_get($notification, 'emails', null); @@ -174,7 +184,8 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack return $this->getNotificationSettings('email')?->isEnabled() || $this->getNotificationSettings('discord')?->isEnabled() || $this->getNotificationSettings('slack')?->isEnabled() || - $this->getNotificationSettings('telegram')?->isEnabled(); + $this->getNotificationSettings('telegram')?->isEnabled() || + $this->getNotificationSettings('pushover')?->isEnabled(); } public function subscriptionEnded() @@ -276,4 +287,9 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack { return $this->hasOne(SlackNotificationSettings::class); } + + public function pushoverNotificationSettings() + { + return $this->hasOne(PushoverNotificationSettings::class); + } } diff --git a/app/Models/TelegramNotificationSettings.php b/app/Models/TelegramNotificationSettings.php index 2edca14ff..78bd841bd 100644 --- a/app/Models/TelegramNotificationSettings.php +++ b/app/Models/TelegramNotificationSettings.php @@ -30,17 +30,17 @@ class TelegramNotificationSettings extends Model 'server_reachable_telegram_notifications', 'server_unreachable_telegram_notifications', - 'telegram_notifications_deployment_success_topic_id', - 'telegram_notifications_deployment_failure_topic_id', - 'telegram_notifications_status_change_topic_id', - 'telegram_notifications_backup_success_topic_id', - 'telegram_notifications_backup_failure_topic_id', - 'telegram_notifications_scheduled_task_success_topic_id', - 'telegram_notifications_scheduled_task_failure_topic_id', - 'telegram_notifications_docker_cleanup_topic_id', - 'telegram_notifications_server_disk_usage_topic_id', - 'telegram_notifications_server_reachable_topic_id', - 'telegram_notifications_server_unreachable_topic_id', + 'telegram_notifications_deployment_success_thread_id', + 'telegram_notifications_deployment_failure_thread_id', + 'telegram_notifications_status_change_thread_id', + 'telegram_notifications_backup_success_thread_id', + 'telegram_notifications_backup_failure_thread_id', + 'telegram_notifications_scheduled_task_success_thread_id', + 'telegram_notifications_scheduled_task_failure_thread_id', + 'telegram_notifications_docker_cleanup_thread_id', + 'telegram_notifications_server_disk_usage_thread_id', + 'telegram_notifications_server_reachable_thread_id', + 'telegram_notifications_server_unreachable_thread_id', ]; protected $casts = [ @@ -60,17 +60,17 @@ class TelegramNotificationSettings extends Model 'server_reachable_telegram_notifications' => 'boolean', 'server_unreachable_telegram_notifications' => 'boolean', - 'telegram_notifications_deployment_success_topic_id' => 'encrypted', - 'telegram_notifications_deployment_failure_topic_id' => 'encrypted', - 'telegram_notifications_status_change_topic_id' => 'encrypted', - 'telegram_notifications_backup_success_topic_id' => 'encrypted', - 'telegram_notifications_backup_failure_topic_id' => 'encrypted', - 'telegram_notifications_scheduled_task_success_topic_id' => 'encrypted', - 'telegram_notifications_scheduled_task_failure_topic_id' => 'encrypted', - 'telegram_notifications_docker_cleanup_topic_id' => 'encrypted', - 'telegram_notifications_server_disk_usage_topic_id' => 'encrypted', - 'telegram_notifications_server_reachable_topic_id' => 'encrypted', - 'telegram_notifications_server_unreachable_topic_id' => 'encrypted', + 'telegram_notifications_deployment_success_thread_id' => 'encrypted', + 'telegram_notifications_deployment_failure_thread_id' => 'encrypted', + 'telegram_notifications_status_change_thread_id' => 'encrypted', + 'telegram_notifications_backup_success_thread_id' => 'encrypted', + 'telegram_notifications_backup_failure_thread_id' => 'encrypted', + 'telegram_notifications_scheduled_task_success_thread_id' => 'encrypted', + 'telegram_notifications_scheduled_task_failure_thread_id' => 'encrypted', + 'telegram_notifications_docker_cleanup_thread_id' => 'encrypted', + 'telegram_notifications_server_disk_usage_thread_id' => 'encrypted', + 'telegram_notifications_server_reachable_thread_id' => 'encrypted', + 'telegram_notifications_server_unreachable_thread_id' => 'encrypted', ]; public function team() diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 06a2b48e3..80c1c421c 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -6,6 +6,7 @@ use App\Models\Application; use App\Models\ApplicationPreview; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -130,6 +131,31 @@ class DeploymentFailed extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + if ($this->preview) { + $title = "Pull request #{$this->preview->pull_request_id} deployment failed"; + $message = "Pull request deployment failed for {$this->application_name}"; + } else { + $title = 'Deployment failed'; + $message = "Deployment failed for {$this->application_name}"; + } + + $buttons[] = [ + 'text' => 'Deployment logs', + 'url' => $this->deployment_url, + ]; + + return new PushoverMessage( + title: $title, + level: 'error', + message: $message, + buttons: [ + ...$buttons, + ], + ); + } + public function toSlack(): SlackMessage { if ($this->preview) { diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 548f5c6e5..b1a3d5225 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -6,6 +6,7 @@ use App\Models\Application; use App\Models\ApplicationPreview; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -139,6 +140,42 @@ class DeploymentSuccess extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + if ($this->preview) { + $title = "Pull request #{$this->preview->pull_request_id} successfully deployed"; + $message = 'New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ''; + if ($this->preview->fqdn) { + $buttons[] = [ + 'text' => 'Open Application', + 'url' => $this->preview->fqdn, + ]; + } + } else { + $title = 'New version successfully deployed'; + $message = 'New version successfully deployed of ' . $this->application_name . ''; + if ($this->fqdn) { + $buttons[] = [ + 'text' => 'Open Application', + 'url' => $this->fqdn, + ]; + } + } + $buttons[] = [ + 'text' => 'Deployment logs', + 'url' => $this->deployment_url, + ]; + + return new PushoverMessage( + title: $title, + level: 'success', + message: $message, + buttons: [ + ...$buttons, + ], + ); + } + public function toSlack(): SlackMessage { if ($this->preview) { diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 32a6a659a..c9c7344c4 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -5,6 +5,7 @@ namespace App\Notifications\Application; use App\Models\Application; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -77,6 +78,23 @@ class StatusChanged extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + $message = $this->resource_name . ' has been stopped.'; + + return new PushoverMessage( + title: 'Application stopped', + level: 'error', + message: $message, + buttons: [ + [ + 'text' => 'Open Application in Coolify', + 'url' => $this->resource_url, + ], + ], + ); + } + public function toSlack(): SlackMessage { $title = 'Application stopped'; diff --git a/app/Notifications/Channels/PushoverChannel.php b/app/Notifications/Channels/PushoverChannel.php new file mode 100644 index 000000000..3d3728d01 --- /dev/null +++ b/app/Notifications/Channels/PushoverChannel.php @@ -0,0 +1,21 @@ +toPushover(); + $pushoverSettings = $notifiable->pushoverNotificationSettings; + + if (! $pushoverSettings || ! $pushoverSettings->isEnabled() || ! $pushoverSettings->pushover_user_key || ! $pushoverSettings->pushover_api_token) { + return; + } + + SendMessageToPushoverJob::dispatch($message, $pushoverSettings->pushover_api_token, $pushoverSettings->pushover_user_key); + } +} diff --git a/app/Notifications/Channels/SendsPushover.php b/app/Notifications/Channels/SendsPushover.php new file mode 100644 index 000000000..7922eefb4 --- /dev/null +++ b/app/Notifications/Channels/SendsPushover.php @@ -0,0 +1,8 @@ +telegram_token; $chatId = $settings->telegram_chat_id; - $topicId = match (get_class($notification)) { - \App\Notifications\Test::class => $settings->telegram_notifications_test_topic_id, + $threadId = match (get_class($notification)) { + \App\Notifications\Application\DeploymentSuccess::class => $settings->telegram_notifications_deployment_success_thread_id, + \App\Notifications\Application\DeploymentFailed::class => $settings->telegram_notifications_deployment_failure_thread_id, \App\Notifications\Application\StatusChanged::class, \App\Notifications\Container\ContainerRestarted::class, - \App\Notifications\Container\ContainerStopped::class => $settings->telegram_notifications_status_change_topic_id, - \App\Notifications\Application\DeploymentSuccess::class => $settings->telegram_notifications_deployment_success_topic_id, - \App\Notifications\Application\DeploymentFailed::class => $settings->telegram_notifications_deployment_failure_topic_id, - \App\Notifications\Database\BackupSuccess::class => $settings->telegram_notifications_backup_success_topic_id, - \App\Notifications\Database\BackupFailed::class => $settings->telegram_notifications_backup_failure_topic_id, - \App\Notifications\ScheduledTask\TaskFailed::class => $settings->telegram_notifications_scheduled_task_failure_topic_id, - \App\Notifications\Server\Unreachable::class => $settings->telegram_notifications_server_unreachable_topic_id, - \App\Notifications\Server\Reachable::class => $settings->telegram_notifications_server_reachable_topic_id, + \App\Notifications\Container\ContainerStopped::class => $settings->telegram_notifications_status_change_thread_id, + + \App\Notifications\Database\BackupSuccess::class => $settings->telegram_notifications_backup_success_thread_id, + \App\Notifications\Database\BackupFailed::class => $settings->telegram_notifications_backup_failure_thread_id, + + \App\Notifications\ScheduledTask\TaskSuccess::class => $settings->telegram_notifications_scheduled_task_success_thread_id, + \App\Notifications\ScheduledTask\TaskFailed::class => $settings->telegram_notifications_scheduled_task_failure_thread_id, + + \App\Notifications\Server\DockerCleanupSuccess::class => $settings->telegram_notifications_docker_cleanup_success_thread_id, + \App\Notifications\Server\DockerCleanupFailed::class => $settings->telegram_notifications_docker_cleanup_failure_thread_id, + \App\Notifications\Server\HighDiskUsage::class => $settings->telegram_notifications_server_disk_usage_thread_id, + \App\Notifications\Server\Unreachable::class => $settings->telegram_notifications_server_unreachable_thread_id, + \App\Notifications\Server\Reachable::class => $settings->telegram_notifications_server_reachable_thread_id, + default => null, }; @@ -35,6 +42,6 @@ class TelegramChannel return; } - SendMessageToTelegramJob::dispatch($message, $buttons, $telegramToken, $chatId, $topicId); + SendMessageToTelegramJob::dispatch($message, $buttons, $telegramToken, $chatId, $threadId); } } diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index c25072ecf..68fc6b019 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -5,6 +5,7 @@ namespace App\Notifications\Container; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -68,6 +69,24 @@ class ContainerRestarted extends CustomEmailNotification return $payload; } + public function toPushover(): PushoverMessage + { + $buttons = []; + if ($this->url) { + $buttons[] = [ + 'text' => 'Check Proxy in Coolify', + 'url' => $this->url, + ]; + } + + return new PushoverMessage( + title: 'Resource restarted', + level: 'warning', + message: "A resource ({$this->name}) has been restarted automatically on {$this->server->name}", + buttons: $buttons, + ); + } + public function toSlack(): SlackMessage { $title = 'Resource restarted'; diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index bc6e52b6d..49aea196d 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -5,6 +5,7 @@ namespace App\Notifications\Container; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -68,6 +69,25 @@ class ContainerStopped extends CustomEmailNotification return $payload; } + public function toPushover(): PushoverMessage + { + $buttons = []; + if ($this->url) { + $buttons[] = [ + 'text' => 'Open Application in Coolify', + 'url' => $this->url, + ]; + } + + return new PushoverMessage( + title: 'Resource stopped', + level: 'error', + message: "A resource ({$this->name}) has been stopped unexpectedly on {$this->server->name}", + buttons: $buttons, + ); + } + + public function toSlack(): SlackMessage { $title = 'Resource stopped'; diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 2208f7b1d..6dcb70583 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -5,6 +5,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -64,6 +65,15 @@ class BackupFailed extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Database backup failed', + level: 'error', + message: "Database backup for {$this->name} (db:{$this->database_name}) was FAILED

Frequency: {$this->frequency} .
Reason: {$this->output}", + ); + } + public function toSlack(): SlackMessage { $title = 'Database backup failed'; diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index 10b4ff3df..4c3e8e060 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -5,6 +5,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -62,6 +63,17 @@ class BackupSuccess extends CustomEmailNotification ]; } + + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Database backup successful', + level: 'success', + message: "Database backup for {$this->name} (db:{$this->database_name}) was successful.

Frequency: {$this->frequency}.", + ); + } + + public function toSlack(): SlackMessage { $title = 'Database backup successful'; diff --git a/app/Notifications/Dto/PushoverMessage.php b/app/Notifications/Dto/PushoverMessage.php new file mode 100644 index 000000000..0efd1d526 --- /dev/null +++ b/app/Notifications/Dto/PushoverMessage.php @@ -0,0 +1,50 @@ +level) { + 'info' => 'ℹ️', + 'error' => '❌', + 'success' => '✅ ', + 'warning' => '⚠️', + }; + } + + public function toPayload(string $token, string $user): array + { + $levelIcon = $this->getLevelIcon(); + $payload = [ + 'token' => $token, + 'user' => $user, + 'title' => "{$levelIcon} {$this->title}", + 'message' => $this->message, + 'html' => 1, + ]; + + foreach ($this->buttons as $button) { + $buttonUrl = data_get($button, 'url'); + $text = data_get($button, 'text', 'Click here'); + if ($buttonUrl && str_contains($buttonUrl, 'http://localhost')) { + $buttonUrl = str_replace('http://localhost', config('app.url'), $buttonUrl); + } + $payload['message'] .= " " . $text . ''; + } + + Log::info('Pushover message', $payload); + + return $payload; + } +} diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index 7049e3055..1d2367210 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -3,6 +3,7 @@ namespace App\Notifications\Internal; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -40,6 +41,15 @@ class GeneralNotification extends Notification implements ShouldQueue ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'General Notification', + level: 'info', + message: $this->message, + ); + } + public function toSlack(): SlackMessage { return new SlackMessage( diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index 56c410ecb..c4d92f213 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -5,6 +5,7 @@ namespace App\Notifications\ScheduledTask; use App\Models\ScheduledTask; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -70,6 +71,30 @@ class TaskFailed extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + $message = "Scheduled task ({$this->task->name}) failed
"; + + if ($this->output) { + $message .= "
Error Output:{$this->output}"; + } + + $buttons = []; + if ($this->url) { + $buttons[] = [ + 'text' => 'Open task in Coolify', + 'url' => (string) $this->url, + ]; + } + + return new PushoverMessage( + title: 'Scheduled task failed', + level: 'error', + message: $message, + buttons: $buttons, + ); + } + public function toSlack(): SlackMessage { $title = 'Scheduled task failed'; diff --git a/app/Notifications/ScheduledTask/TaskSuccess.php b/app/Notifications/ScheduledTask/TaskSuccess.php index fc79aea37..5d4154e7a 100644 --- a/app/Notifications/ScheduledTask/TaskSuccess.php +++ b/app/Notifications/ScheduledTask/TaskSuccess.php @@ -5,6 +5,7 @@ namespace App\Notifications\ScheduledTask; use App\Models\ScheduledTask; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -70,6 +71,25 @@ class TaskSuccess extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + $message = "Coolify: Scheduled task ({$this->task->name}) succeeded."; + $buttons = []; + if ($this->url) { + $buttons[] = [ + 'text' => 'Open task in Coolify', + 'url' => (string) $this->url, + ]; + } + + return new PushoverMessage( + title: 'Scheduled task succeeded', + level: 'success', + message: $message, + buttons: $buttons, + ); + } + public function toSlack(): SlackMessage { $title = 'Scheduled task succeeded'; diff --git a/app/Notifications/Server/DockerCleanupFailed.php b/app/Notifications/Server/DockerCleanupFailed.php index 53714925c..0291eed19 100644 --- a/app/Notifications/Server/DockerCleanupFailed.php +++ b/app/Notifications/Server/DockerCleanupFailed.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -48,6 +49,15 @@ class DockerCleanupFailed extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Docker cleanup job failed', + level: 'error', + message: "[ACTION REQUIRED] Docker cleanup job failed on {$this->server->name}!\n\n{$this->message}", + ); + } + public function toSlack(): SlackMessage { return new SlackMessage( diff --git a/app/Notifications/Server/DockerCleanupSuccess.php b/app/Notifications/Server/DockerCleanupSuccess.php index 85a819da2..1a652d189 100644 --- a/app/Notifications/Server/DockerCleanupSuccess.php +++ b/app/Notifications/Server/DockerCleanupSuccess.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -48,6 +49,15 @@ class DockerCleanupSuccess extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Docker cleanup job succeeded', + level: 'success', + message: "Docker cleanup job succeeded on {$this->server->name}!\n\n{$this->message}", + ); + } + public function toSlack(): SlackMessage { return new SlackMessage( diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index e122849da..7a1f7bcbf 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -51,6 +52,15 @@ class ForceDisabled extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Server disabled', + level: 'error', + message: "Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.
Please update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).", + ); + } + public function toSlack(): SlackMessage { $title = 'Server disabled'; diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index a0e0a8b59..36dad3c60 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -47,6 +48,15 @@ class ForceEnabled extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Server enabled', + level: 'success', + message: "Server ({$this->server->name}) enabled again!", + ); + } + public function toSlack(): SlackMessage { return new SlackMessage( diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 9f4826689..aea9abd03 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -57,6 +58,19 @@ class HighDiskUsage extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'High disk usage detected', + level: 'warning', + message: "Server '{$this->server->name}' high disk usage detected!

Disk usage: {$this->disk_usage}%.
Threshold: {$this->server_disk_usage_notification_threshold}%.
Please cleanup your disk to prevent data-loss.", + buttons: [ + 'Change settings' => base_url().'/server/'.$this->server->uuid."#advanced", + 'Tips for cleanup' => "https://coolify.io/docs/knowledge-base/server/automated-cleanup", + ], + ); + } + public function toSlack(): SlackMessage { $description = "Server '{$this->server->name}' high disk usage detected!\n"; diff --git a/app/Notifications/Server/Reachable.php b/app/Notifications/Server/Reachable.php index 4917e04f8..e03aef6b7 100644 --- a/app/Notifications/Server/Reachable.php +++ b/app/Notifications/Server/Reachable.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -49,6 +50,15 @@ class Reachable extends CustomEmailNotification ); } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Server revived', + message: "Server '{$this->server->name}' revived. All automations & integrations are turned on again!", + level: 'success', + ); + } + public function toTelegram(): array { return [ diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 43f176d49..fe90cc610 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -5,6 +5,7 @@ namespace App\Notifications\Server; use App\Models\Server; use App\Notifications\CustomEmailNotification; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Notifications\Messages\MailMessage; @@ -60,6 +61,15 @@ class Unreachable extends CustomEmailNotification ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Server unreachable', + level: 'error', + message: "Your server '{$this->server->name}' is unreachable.
All automations & integrations are turned off!

IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", + ); + } + public function toSlack(): SlackMessage { $description = "Your server '{$this->server->name}' is unreachable.\n"; diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index da9098500..65971a0ee 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -6,7 +6,9 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\PushoverChannel; use App\Notifications\Dto\DiscordMessage; +use App\Notifications\Dto\PushoverMessage; use App\Notifications\Dto\SlackMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -33,6 +35,7 @@ class Test extends Notification implements ShouldQueue 'discord' => [DiscordChannel::class], 'telegram' => [TelegramChannel::class], 'slack' => [SlackChannel::class], + 'pushover' => [PushoverChannel::class], default => [], }; } else { @@ -85,6 +88,20 @@ class Test extends Notification implements ShouldQueue ]; } + public function toPushover(): PushoverMessage + { + return new PushoverMessage( + title: 'Test Pushover Notification', + message: 'This is a test Pushover notification from Coolify.', + buttons: [ + [ + 'text' => 'Go to your dashboard', + 'url' => base_url(), + ], + ], + ); + } + public function toSlack(): SlackMessage { return new SlackMessage( diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 015434bd2..2ce94201c 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Models\PersonalAccessToken; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rules\Password; @@ -19,6 +20,9 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { + Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { + $event->extendSocialite('authentik', \SocialiteProviders\Authentik\Provider::class); + }); Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); Password::defaults(function () { diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 7ba72e10d..aa3579f8d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -9,6 +9,9 @@ use App\Listeners\ProxyStartedNotification; use Illuminate\Foundation\Events\MaintenanceModeDisabled; use Illuminate\Foundation\Events\MaintenanceModeEnabled; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use SocialiteProviders\Authentik\AuthentikExtendSocialite; +use SocialiteProviders\Azure\AzureExtendSocialite; +use SocialiteProviders\Manager\SocialiteWasCalled; class EventServiceProvider extends ServiceProvider { @@ -19,8 +22,9 @@ class EventServiceProvider extends ServiceProvider MaintenanceModeDisabled::class => [ MaintenanceModeDisabledNotification::class, ], - \SocialiteProviders\Manager\SocialiteWasCalled::class => [ - \SocialiteProviders\Azure\AzureExtendSocialite::class.'@handle', + SocialiteWasCalled::class => [ + AzureExtendSocialite::class.'@handle', + AuthentikExtendSocialite::class.'@handle', ], ProxyStarted::class => [ ProxyStartedNotification::class, diff --git a/app/Traits/HasNotificationSettings.php b/app/Traits/HasNotificationSettings.php index 82cbda6ad..ef858d0b6 100644 --- a/app/Traits/HasNotificationSettings.php +++ b/app/Traits/HasNotificationSettings.php @@ -6,6 +6,7 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\SlackChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Channels\PushoverChannel; use Illuminate\Database\Eloquent\Model; trait HasNotificationSettings @@ -27,6 +28,7 @@ trait HasNotificationSettings 'discord' => $this->discordNotificationSettings, 'telegram' => $this->telegramNotificationSettings, 'slack' => $this->slackNotificationSettings, + 'pushover' => $this->pushoverNotificationSettings, default => null, }; } @@ -73,6 +75,7 @@ trait HasNotificationSettings 'discord' => DiscordChannel::class, 'telegram' => TelegramChannel::class, 'slack' => SlackChannel::class, + 'pushover' => PushoverChannel::class, ]; if ($event === 'general') { diff --git a/app/View/Components/Status/Services.php b/app/View/Components/Status/Services.php index 70db62172..291841854 100644 --- a/app/View/Components/Status/Services.php +++ b/app/View/Components/Status/Services.php @@ -17,7 +17,7 @@ class Services extends Component public string $complexStatus = 'exited', public bool $showRefreshButton = true ) { - $this->complexStatus = $service->status(); + $this->complexStatus = $service->status; } /** diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 9f7cc06b0..eeb89addc 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -830,6 +830,12 @@ function generateEnvValue(string $command, Service|Application|null $service = n case 'PASSWORD_64': $generatedValue = Str::password(length: 64, symbols: false); break; + case 'PASSWORDWITHSYMBOLS': + $generatedValue = Str::password(symbols: true); + break; + case 'PASSWORDWITHSYMBOLS_64': + $generatedValue = Str::password(length: 64, symbols: true); + break; // This is not base64, it's just a random string case 'BASE64_64': $generatedValue = Str::random(64); diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php index cad9de7fa..130227e81 100644 --- a/bootstrap/helpers/socialite.php +++ b/bootstrap/helpers/socialite.php @@ -18,6 +18,17 @@ function get_socialite_provider(string $provider) return Socialite::driver('azure')->setConfig($azure_config); } + if ($provider == 'authentik') { + $authentik_config = new \SocialiteProviders\Manager\Config( + $oauth_setting->client_id, + $oauth_setting->client_secret, + $oauth_setting->redirect_uri, + ['base_url' => $oauth_setting->base_url], + ); + + return Socialite::driver('authentik')->setConfig($authentik_config); + } + $config = [ 'client_id' => $oauth_setting->client_id, 'client_secret' => $oauth_setting->client_secret, diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index ab9ee9b9d..510516a2f 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -69,6 +69,7 @@ function allowedPathsForUnsubscribedAccounts() 'logout', 'force-password-reset', 'livewire/update', + 'admin', ]; } function allowedPathsForBoardingAccounts() diff --git a/composer.json b/composer.json index d1c6f5e4f..b8dc354c3 100644 --- a/composer.json +++ b/composer.json @@ -39,6 +39,7 @@ "pusher/pusher-php-server": "^7.2", "resend/resend-laravel": "^0.15.0", "sentry/sentry-laravel": "^4.6", + "socialiteproviders/authentik": "^5.2", "socialiteproviders/microsoft-azure": "^5.1", "spatie/laravel-activitylog": "^4.7.3", "spatie/laravel-data": "^4.11", diff --git a/composer.lock b/composer.lock index 68044ab56..3fbe72afb 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": "d96fe0aedd865274164c34a1b04195fe", + "content-hash": "871067cb42e6347ca53ff36e81ac5079", "packages": [ { "name": "3sidedcube/laravel-redoc", @@ -979,16 +979,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.334.2", + "version": "3.334.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b19afc076bb1cc2617bdef76efd41587596109e7" + "reference": "6576a9fcfc6ae7c76aed3c6fa4c3864060f72d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b19afc076bb1cc2617bdef76efd41587596109e7", - "reference": "b19afc076bb1cc2617bdef76efd41587596109e7", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6576a9fcfc6ae7c76aed3c6fa4c3864060f72d04", + "reference": "6576a9fcfc6ae7c76aed3c6fa4c3864060f72d04", "shasum": "" }, "require": { @@ -1071,9 +1071,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.334.2" + "source": "https://github.com/aws/aws-sdk-php/tree/3.334.3" }, - "time": "2024-12-09T19:30:23+00:00" + "time": "2024-12-10T19:41:55+00:00" }, { "name": "bacon/bacon-qr-code", @@ -3313,16 +3313,16 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "5.x-dev" - }, "laravel": { - "providers": [ - "Laravel\\Socialite\\SocialiteServiceProvider" - ], "aliases": { "Socialite": "Laravel\\Socialite\\Facades\\Socialite" - } + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" } }, "autoload": { @@ -4037,16 +4037,16 @@ }, { "name": "league/oauth1-client", - "version": "v1.10.1", + "version": "v1.11.0", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth1-client.git", - "reference": "d6365b901b5c287dd41f143033315e2f777e1167" + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", - "reference": "d6365b901b5c287dd41f143033315e2f777e1167", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", "shasum": "" }, "require": { @@ -4107,9 +4107,9 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth1-client/issues", - "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1" + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.11.0" }, - "time": "2022-04-15T14:02:14+00:00" + "time": "2024-12-10T19:59:05+00:00" }, { "name": "league/uri", @@ -5952,64 +5952,6 @@ }, "time": "2024-10-13T11:29:49+00:00" }, - { - "name": "phpstan/phpstan", - "version": "1.12.12", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0" - }, - "conflict": { - "phpstan/phpstan-shim": "*" - }, - "bin": [ - "phpstan", - "phpstan.phar" - ], - "type": "library", - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHPStan - PHP Static Analysis Tool", - "keywords": [ - "dev", - "static analysis" - ], - "support": { - "docs": "https://phpstan.org/user-guide/getting-started", - "forum": "https://github.com/phpstan/phpstan/discussions", - "issues": "https://github.com/phpstan/phpstan/issues", - "security": "https://github.com/phpstan/phpstan/security/policy", - "source": "https://github.com/phpstan/phpstan-src" - }, - "funding": [ - { - "url": "https://github.com/ondrejmirtes", - "type": "github" - }, - { - "url": "https://github.com/phpstan", - "type": "github" - } - ], - "time": "2024-11-28T22:13:23+00:00" - }, { "name": "pimple/pimple", "version": "v3.5.0", @@ -7099,65 +7041,6 @@ ], "time": "2024-04-27T21:32:50+00:00" }, - { - "name": "rector/rector", - "version": "1.2.10", - "source": { - "type": "git", - "url": "https://github.com/rectorphp/rector.git", - "reference": "40f9cf38c05296bd32f444121336a521a293fa61" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/40f9cf38c05296bd32f444121336a521a293fa61", - "reference": "40f9cf38c05296bd32f444121336a521a293fa61", - "shasum": "" - }, - "require": { - "php": "^7.2|^8.0", - "phpstan/phpstan": "^1.12.5" - }, - "conflict": { - "rector/rector-doctrine": "*", - "rector/rector-downgrade-php": "*", - "rector/rector-phpunit": "*", - "rector/rector-symfony": "*" - }, - "suggest": { - "ext-dom": "To manipulate phpunit.xml via the custom-rule command" - }, - "bin": [ - "bin/rector" - ], - "type": "library", - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Instant Upgrade and Automated Refactoring of any PHP code", - "keywords": [ - "automation", - "dev", - "migration", - "refactoring" - ], - "support": { - "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/1.2.10" - }, - "funding": [ - { - "url": "https://github.com/tomasvotruba", - "type": "github" - } - ], - "time": "2024-11-08T13:59:10+00:00" - }, { "name": "resend/resend-laravel", "version": "v0.15.0", @@ -7534,6 +7417,56 @@ ], "time": "2024-11-24T11:02:20+00:00" }, + { + "name": "socialiteproviders/authentik", + "version": "5.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Authentik.git", + "reference": "4cf129cf04728a38e0531c54454464b162f0fa66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Authentik/zipball/4cf129cf04728a38e0531c54454464b162f0fa66", + "reference": "4cf129cf04728a38e0531c54454464b162f0fa66", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.0", + "socialiteproviders/manager": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Authentik\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "rf152", + "email": "git@rf152.co.uk" + } + ], + "description": "Authentik OAuth2 Provider for Laravel Socialite", + "keywords": [ + "authentik", + "laravel", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/authentik", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2023-11-07T22:21:16+00:00" + }, { "name": "socialiteproviders/manager", "version": "v4.7.0", @@ -7959,40 +7892,41 @@ }, { "name": "spatie/laravel-ray", - "version": "1.37.1", + "version": "1.39.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-ray.git", - "reference": "c2bedfd1172648df2c80aaceb2541d70f1d9a5b9" + "reference": "31b601f98590606d20e76b5dd68578dc1642cd2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/c2bedfd1172648df2c80aaceb2541d70f1d9a5b9", - "reference": "c2bedfd1172648df2c80aaceb2541d70f1d9a5b9", + "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/31b601f98590606d20e76b5dd68578dc1642cd2c", + "reference": "31b601f98590606d20e76b5dd68578dc1642cd2c", "shasum": "" }, "require": { + "composer-runtime-api": "^2.2", "ext-json": "*", - "illuminate/contracts": "^7.20|^8.19|^9.0|^10.0|^11.0", - "illuminate/database": "^7.20|^8.19|^9.0|^10.0|^11.0", - "illuminate/queue": "^7.20|^8.19|^9.0|^10.0|^11.0", - "illuminate/support": "^7.20|^8.19|^9.0|^10.0|^11.0", - "php": "^7.4|^8.0", - "rector/rector": "^0.19.2|^1.0", + "illuminate/contracts": "^7.20 || ^8.19 || ^9.0 || ^10.0 || ^11.0", + "illuminate/database": "^7.20 || ^8.19 || ^9.0 || ^10.0 || ^11.0", + "illuminate/queue": "^7.20 || ^8.19 || ^9.0 || ^10.0 || ^11.0", + "illuminate/support": "^7.20 || ^8.19 || ^9.0 || ^10.0 || ^11.0", + "php": "^7.4 || ^8.0", "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|^3.0" + "spatie/ray": "^1.41.3", + "symfony/stopwatch": "4.2 || ^5.1 || ^6.0 || ^7.0", + "zbateson/mail-mime-parser": "^1.3.1 || ^2.0 || ^3.0" }, "require-dev": { "guzzlehttp/guzzle": "^7.3", - "laravel/framework": "^7.20|^8.19|^9.0|^10.0|^11.0", - "orchestra/testbench-core": "^5.0|^6.0|^7.0|^8.0|^9.0", - "pestphp/pest": "^1.22|^2.0", - "phpstan/phpstan": "^1.10.57", - "phpunit/phpunit": "^9.3|^10.1", - "spatie/pest-plugin-snapshots": "^1.1|^2.0", - "symfony/var-dumper": "^4.2|^5.1|^6.0|^7.0.3" + "laravel/framework": "^7.20 || ^8.19 || ^9.0 || ^10.0 || ^11.0", + "orchestra/testbench-core": "^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "pestphp/pest": "^1.22 || ^2.0", + "phpstan/phpstan": "^1.10.57 || ^2.0.2", + "phpunit/phpunit": "^9.3 || ^10.1", + "rector/rector": "dev-main", + "spatie/pest-plugin-snapshots": "^1.1 || ^2.0", + "symfony/var-dumper": "^4.2 || ^5.1 || ^6.0 || ^7.0.3" }, "type": "library", "extra": { @@ -8030,7 +7964,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-ray/issues", - "source": "https://github.com/spatie/laravel-ray/tree/1.37.1" + "source": "https://github.com/spatie/laravel-ray/tree/1.39.0" }, "funding": [ { @@ -8042,7 +7976,7 @@ "type": "other" } ], - "time": "2024-07-12T12:35:17+00:00" + "time": "2024-12-11T09:34:41+00:00" }, { "name": "spatie/laravel-schemaless-attributes", @@ -8532,16 +8466,16 @@ }, { "name": "symfony/console", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf" + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", - "reference": "23c8aae6d764e2bae02d2a99f7532a7f6ed619cf", + "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", "shasum": "" }, "require": { @@ -8605,7 +8539,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.0" + "source": "https://github.com/symfony/console/tree/v7.2.1" }, "funding": [ { @@ -8621,7 +8555,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2024-12-11T03:49:26+00:00" }, { "name": "symfony/css-selector", @@ -8757,16 +8691,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "672b3dd1ef8b87119b446d67c58c106c43f965fe" + "reference": "6150b89186573046167796fa5f3f76601d5145f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/672b3dd1ef8b87119b446d67c58c106c43f965fe", - "reference": "672b3dd1ef8b87119b446d67c58c106c43f965fe", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/6150b89186573046167796fa5f3f76601d5145f8", + "reference": "6150b89186573046167796fa5f3f76601d5145f8", "shasum": "" }, "require": { @@ -8812,7 +8746,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/v7.2.0" + "source": "https://github.com/symfony/error-handler/tree/v7.2.1" }, "funding": [ { @@ -8828,7 +8762,7 @@ "type": "tidelift" } ], - "time": "2024-11-05T15:35:02+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/event-dispatcher", @@ -9130,16 +9064,16 @@ }, { "name": "symfony/http-kernel", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6b4722a25e0aed1ccb4914b9bcbd493cc4676b4d" + "reference": "d8ae58eecae44c8e66833e76cc50a4ad3c002d97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6b4722a25e0aed1ccb4914b9bcbd493cc4676b4d", - "reference": "6b4722a25e0aed1ccb4914b9bcbd493cc4676b4d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/d8ae58eecae44c8e66833e76cc50a4ad3c002d97", + "reference": "d8ae58eecae44c8e66833e76cc50a4ad3c002d97", "shasum": "" }, "require": { @@ -9224,7 +9158,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/v7.2.0" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.1" }, "funding": [ { @@ -9240,7 +9174,7 @@ "type": "tidelift" } ], - "time": "2024-11-29T08:42:40+00:00" + "time": "2024-12-11T12:09:10+00:00" }, { "name": "symfony/mailer", @@ -9324,16 +9258,16 @@ }, { "name": "symfony/mime", - "version": "v7.2.0", + "version": "v7.2.1", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d" + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/cc84a4b81f62158c3846ac7ff10f696aae2b524d", - "reference": "cc84a4b81f62158c3846ac7ff10f696aae2b524d", + "url": "https://api.github.com/repos/symfony/mime/zipball/7f9617fcf15cb61be30f8b252695ed5e2bfac283", + "reference": "7f9617fcf15cb61be30f8b252695ed5e2bfac283", "shasum": "" }, "require": { @@ -9388,7 +9322,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.0" + "source": "https://github.com/symfony/mime/tree/v7.2.1" }, "funding": [ { @@ -9404,7 +9338,7 @@ "type": "tidelift" } ], - "time": "2024-11-23T09:19:39+00:00" + "time": "2024-12-07T08:50:44+00:00" }, { "name": "symfony/options-resolver", @@ -13222,17 +13156,75 @@ "time": "2024-11-21T15:12:59+00:00" }, { - "name": "phpunit/php-code-coverage", - "version": "11.0.7", + "name": "phpstan/phpstan", + "version": "1.12.12", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f7f08030e8811582cc459871d28d6f5a1a4d35ca" + "url": "https://github.com/phpstan/phpstan.git", + "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f7f08030e8811582cc459871d28d6f5a1a4d35ca", - "reference": "f7f08030e8811582cc459871d28d6f5a1a4d35ca", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", + "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-11-28T22:13:23+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", + "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", "shasum": "" }, "require": { @@ -13251,7 +13243,7 @@ "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^11.4.1" + "phpunit/phpunit": "^11.5.0" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -13289,7 +13281,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/11.0.7" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8" }, "funding": [ { @@ -13297,7 +13289,7 @@ "type": "github" } ], - "time": "2024-10-09T06:21:38+00:00" + "time": "2024-12-11T12:34:27+00:00" }, { "name": "phpunit/php-file-iterator", @@ -14615,16 +14607,16 @@ }, { "name": "spatie/error-solutions", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/spatie/error-solutions.git", - "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67" + "reference": "d239a65235a1eb128dfa0a4e4c4ef032ea11b541" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/error-solutions/zipball/ae7393122eda72eed7cc4f176d1e96ea444f2d67", - "reference": "ae7393122eda72eed7cc4f176d1e96ea444f2d67", + "url": "https://api.github.com/repos/spatie/error-solutions/zipball/d239a65235a1eb128dfa0a4e4c4ef032ea11b541", + "reference": "d239a65235a1eb128dfa0a4e4c4ef032ea11b541", "shasum": "" }, "require": { @@ -14677,7 +14669,7 @@ ], "support": { "issues": "https://github.com/spatie/error-solutions/issues", - "source": "https://github.com/spatie/error-solutions/tree/1.1.1" + "source": "https://github.com/spatie/error-solutions/tree/1.1.2" }, "funding": [ { @@ -14685,7 +14677,7 @@ "type": "github" } ], - "time": "2024-07-25T11:06:04+00:00" + "time": "2024-12-11T09:51:56+00:00" }, { "name": "spatie/flare-client-php", diff --git a/config/constants.php b/config/constants.php index 4ea4411b6..99859ffb8 100644 --- a/config/constants.php +++ b/config/constants.php @@ -2,7 +2,7 @@ return [ 'coolify' => [ - 'version' => '4.0.0-beta.377', + 'version' => '4.0.0-beta.380', 'self_hosted' => env('SELF_HOSTED', true), 'autoupdate' => env('AUTOUPDATE'), 'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'), @@ -33,6 +33,14 @@ return [ 'app_key' => env('PUSHER_APP_KEY'), ], + 'migration' => [ + 'is_migration_enabled' => env('MIGRATION_ENABLED', true), + ], + + 'seeder' => [ + 'is_seeder_enabled' => env('SEEDER_ENABLED', true), + ], + 'horizon' => [ 'is_horizon_enabled' => env('HORIZON_ENABLED', true), 'is_scheduler_enabled' => env('SCHEDULER_ENABLED', true), diff --git a/config/services.php b/config/services.php index 9fd55870f..46fd12ec3 100644 --- a/config/services.php +++ b/config/services.php @@ -38,4 +38,11 @@ return [ 'tenant' => env('AZURE_TENANT_ID'), 'proxy' => env('AZURE_PROXY'), ], + + 'authentik' => [ + 'base_url' => env('AUTHENTIK_BASE_URL'), + 'client_id' => env('AUTHENTIK_CLIENT_ID'), + 'client_secret' => env('AUTHENTIK_CLIENT_SECRET'), + 'redirect' => env('AUTHENTIK_REDIRECT_URI'), + ], ]; diff --git a/database/migrations/2024_10_30_074601_rename_token_permissions.php b/database/migrations/2024_10_30_074601_rename_token_permissions.php index 2ca98d090..69d774d7f 100644 --- a/database/migrations/2024_10_30_074601_rename_token_permissions.php +++ b/database/migrations/2024_10_30_074601_rename_token_permissions.php @@ -40,7 +40,7 @@ return new class extends Migration $tokens = PersonalAccessToken::all(); foreach ($tokens as $token) { $abilities = collect(); - if (in_array('write', $token->abilities)) { + if (in_array('root', $token->abilities)) { $abilities->push('*'); } else { if (in_array('read', $token->abilities)) { diff --git a/database/migrations/2024_12_05_212440_create_telegram_notification_settings_table.php b/database/migrations/2024_12_05_212440_create_telegram_notification_settings_table.php index 190d148fc..db4d998e3 100644 --- a/database/migrations/2024_12_05_212440_create_telegram_notification_settings_table.php +++ b/database/migrations/2024_12_05_212440_create_telegram_notification_settings_table.php @@ -32,18 +32,18 @@ return new class extends Migration $table->boolean('server_reachable_telegram_notifications')->default(false); $table->boolean('server_unreachable_telegram_notifications')->default(true); - $table->text('telegram_notifications_deployment_success_topic_id')->nullable(); - $table->text('telegram_notifications_deployment_failure_topic_id')->nullable(); - $table->text('telegram_notifications_status_change_topic_id')->nullable(); - $table->text('telegram_notifications_backup_success_topic_id')->nullable(); - $table->text('telegram_notifications_backup_failure_topic_id')->nullable(); - $table->text('telegram_notifications_scheduled_task_success_topic_id')->nullable(); - $table->text('telegram_notifications_scheduled_task_failure_topic_id')->nullable(); - $table->text('telegram_notifications_docker_cleanup_success_topic_id')->nullable(); - $table->text('telegram_notifications_docker_cleanup_failure_topic_id')->nullable(); - $table->text('telegram_notifications_server_disk_usage_topic_id')->nullable(); - $table->text('telegram_notifications_server_reachable_topic_id')->nullable(); - $table->text('telegram_notifications_server_unreachable_topic_id')->nullable(); + $table->text('telegram_notifications_deployment_success_thread_id')->nullable(); + $table->text('telegram_notifications_deployment_failure_thread_id')->nullable(); + $table->text('telegram_notifications_status_change_thread_id')->nullable(); + $table->text('telegram_notifications_backup_success_thread_id')->nullable(); + $table->text('telegram_notifications_backup_failure_thread_id')->nullable(); + $table->text('telegram_notifications_scheduled_task_success_thread_id')->nullable(); + $table->text('telegram_notifications_scheduled_task_failure_thread_id')->nullable(); + $table->text('telegram_notifications_docker_cleanup_success_thread_id')->nullable(); + $table->text('telegram_notifications_docker_cleanup_failure_thread_id')->nullable(); + $table->text('telegram_notifications_server_disk_usage_thread_id')->nullable(); + $table->text('telegram_notifications_server_reachable_thread_id')->nullable(); + $table->text('telegram_notifications_server_unreachable_thread_id')->nullable(); $table->unique(['team_id']); }); diff --git a/database/migrations/2024_12_05_212705_migrate_telegram_notification_settings_from_teams_table.php b/database/migrations/2024_12_05_212705_migrate_telegram_notification_settings_from_teams_table.php index 0c10646b9..df120e273 100644 --- a/database/migrations/2024_12_05_212705_migrate_telegram_notification_settings_from_teams_table.php +++ b/database/migrations/2024_12_05_212705_migrate_telegram_notification_settings_from_teams_table.php @@ -4,6 +4,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -30,17 +31,17 @@ return new class extends Migration 'status_change_telegram_notifications' => $team->telegram_notifications_status_changes ?? false, 'server_disk_usage_telegram_notifications' => $team->telegram_notifications_server_disk_usage ?? true, - 'telegram_notifications_deployment_success_topic_id' => $team->telegram_notifications_deployments_message_thread_id ? Crypt::encryptString($team->telegram_notifications_deployments_message_thread_id) : null, - 'telegram_notifications_deployment_failure_topic_id' => $team->telegram_notifications_deployments_message_thread_id ? Crypt::encryptString($team->telegram_notifications_deployments_message_thread_id) : null, - 'telegram_notifications_backup_success_topic_id' => $team->telegram_notifications_database_backups_message_thread_id ? Crypt::encryptString($team->telegram_notifications_database_backups_message_thread_id) : null, - 'telegram_notifications_backup_failure_topic_id' => $team->telegram_notifications_database_backups_message_thread_id ? Crypt::encryptString($team->telegram_notifications_database_backups_message_thread_id) : null, - 'telegram_notifications_scheduled_task_success_topic_id' => $team->telegram_notifications_scheduled_tasks_thread_id ? Crypt::encryptString($team->telegram_notifications_scheduled_tasks_thread_id) : null, - 'telegram_notifications_scheduled_task_failure_topic_id' => $team->telegram_notifications_scheduled_tasks_thread_id ? Crypt::encryptString($team->telegram_notifications_scheduled_tasks_thread_id) : null, - 'telegram_notifications_status_change_topic_id' => $team->telegram_notifications_status_changes_message_thread_id ? Crypt::encryptString($team->telegram_notifications_status_changes_message_thread_id) : null, + 'telegram_notifications_deployment_success_thread_id' => $team->telegram_notifications_deployments_message_thread_id ? Crypt::encryptString($team->telegram_notifications_deployments_message_thread_id) : null, + 'telegram_notifications_deployment_failure_thread_id' => $team->telegram_notifications_deployments_message_thread_id ? Crypt::encryptString($team->telegram_notifications_deployments_message_thread_id) : null, + 'telegram_notifications_backup_success_thread_id' => $team->telegram_notifications_database_backups_message_thread_id ? Crypt::encryptString($team->telegram_notifications_database_backups_message_thread_id) : null, + 'telegram_notifications_backup_failure_thread_id' => $team->telegram_notifications_database_backups_message_thread_id ? Crypt::encryptString($team->telegram_notifications_database_backups_message_thread_id) : null, + 'telegram_notifications_scheduled_task_success_thread_id' => $team->telegram_notifications_scheduled_tasks_thread_id ? Crypt::encryptString($team->telegram_notifications_scheduled_tasks_thread_id) : null, + 'telegram_notifications_scheduled_task_failure_thread_id' => $team->telegram_notifications_scheduled_tasks_thread_id ? Crypt::encryptString($team->telegram_notifications_scheduled_tasks_thread_id) : null, + 'telegram_notifications_status_change_thread_id' => $team->telegram_notifications_status_changes_message_thread_id ? Crypt::encryptString($team->telegram_notifications_status_changes_message_thread_id) : null, ] ); } catch (Exception $e) { - \Log::error('Error migrating telegram notification settings from teams table: '.$e->getMessage()); + Log::error('Error migrating telegram notification settings from teams table: '.$e->getMessage()); } } @@ -101,13 +102,13 @@ return new class extends Migration 'telegram_notifications_scheduled_tasks' => $setting->scheduled_task_success_telegram_notifications || $setting->scheduled_task_failure_telegram_notifications, 'telegram_notifications_server_disk_usage' => $setting->server_disk_usage_telegram_notifications, - 'telegram_notifications_deployments_message_thread_id' => $setting->telegram_notifications_deployment_success_topic_id ? Crypt::decryptString($setting->telegram_notifications_deployment_success_topic_id) : null, - 'telegram_notifications_status_changes_message_thread_id' => $setting->telegram_notifications_status_change_topic_id ? Crypt::decryptString($setting->telegram_notifications_status_change_topic_id) : null, - 'telegram_notifications_database_backups_message_thread_id' => $setting->telegram_notifications_backup_success_topic_id ? Crypt::decryptString($setting->telegram_notifications_backup_success_topic_id) : null, - 'telegram_notifications_scheduled_tasks_thread_id' => $setting->telegram_notifications_scheduled_task_success_topic_id ? Crypt::decryptString($setting->telegram_notifications_scheduled_task_success_topic_id) : null, + 'telegram_notifications_deployments_message_thread_id' => $setting->telegram_notifications_deployment_success_thread_id ? Crypt::decryptString($setting->telegram_notifications_deployment_success_thread_id) : null, + 'telegram_notifications_status_changes_message_thread_id' => $setting->telegram_notifications_status_change_thread_id ? Crypt::decryptString($setting->telegram_notifications_status_change_thread_id) : null, + 'telegram_notifications_database_backups_message_thread_id' => $setting->telegram_notifications_backup_success_thread_id ? Crypt::decryptString($setting->telegram_notifications_backup_success_thread_id) : null, + 'telegram_notifications_scheduled_tasks_thread_id' => $setting->telegram_notifications_scheduled_task_success_thread_id ? Crypt::decryptString($setting->telegram_notifications_scheduled_task_success_thread_id) : null, ]); } catch (Exception $e) { - \Log::error('Error migrating telegram notification settings from teams table: '.$e->getMessage()); + Log::error('Error migrating telegram notification settings from teams table: '.$e->getMessage()); } } } diff --git a/database/migrations/2024_12_06_142014_create_slack_notification_settings_table.php b/database/migrations/2024_12_06_142014_create_slack_notification_settings_table.php index 790e0f667..8aee40d87 100644 --- a/database/migrations/2024_12_06_142014_create_slack_notification_settings_table.php +++ b/database/migrations/2024_12_06_142014_create_slack_notification_settings_table.php @@ -3,6 +3,7 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; return new class extends Migration @@ -42,7 +43,7 @@ return new class extends Migration 'team_id' => $team->id, ]); } catch (\Throwable $e) { - \Log::error('Error migrating slack notification settings from teams table: '.$e->getMessage()); + Log::error('Error creating slack notification settings for existing teams: '.$e->getMessage()); } } } diff --git a/database/migrations/2024_12_10_122142_encrypt_instance_settings_email_columns.php b/database/migrations/2024_12_10_122142_encrypt_instance_settings_email_columns.php index 44e0f2f47..5602e0ae9 100644 --- a/database/migrations/2024_12_10_122142_encrypt_instance_settings_email_columns.php +++ b/database/migrations/2024_12_10_122142_encrypt_instance_settings_email_columns.php @@ -45,11 +45,11 @@ return new class extends Migration public function down(): void { Schema::table('instance_settings', function (Blueprint $table) { - $table->string('smtp_from_address')->nullable()->change(); - $table->string('smtp_from_name')->nullable()->change(); - $table->string('smtp_recipients')->nullable()->change(); - $table->string('smtp_host')->nullable()->change(); - $table->string('smtp_username')->nullable()->change(); + $table->text('smtp_from_address')->nullable()->change(); + $table->text('smtp_from_name')->nullable()->change(); + $table->text('smtp_recipients')->nullable()->change(); + $table->text('smtp_host')->nullable()->change(); + $table->text('smtp_username')->nullable()->change(); }); if (DB::table('instance_settings')->exists()) { diff --git a/database/migrations/2024_12_11_135026_create_pushover_notification_settings_table.php b/database/migrations/2024_12_11_135026_create_pushover_notification_settings_table.php new file mode 100644 index 000000000..ad4215a07 --- /dev/null +++ b/database/migrations/2024_12_11_135026_create_pushover_notification_settings_table.php @@ -0,0 +1,59 @@ +id(); + $table->foreignId('team_id')->constrained()->cascadeOnDelete(); + + $table->boolean('pushover_enabled')->default(false); + $table->text('pushover_user_key')->nullable(); + $table->text('pushover_api_token')->nullable(); + + $table->boolean('deployment_success_pushover_notifications')->default(false); + $table->boolean('deployment_failure_pushover_notifications')->default(true); + $table->boolean('status_change_pushover_notifications')->default(false); + $table->boolean('backup_success_pushover_notifications')->default(false); + $table->boolean('backup_failure_pushover_notifications')->default(true); + $table->boolean('scheduled_task_success_pushover_notifications')->default(false); + $table->boolean('scheduled_task_failure_pushover_notifications')->default(true); + $table->boolean('docker_cleanup_success_pushover_notifications')->default(false); + $table->boolean('docker_cleanup_failure_pushover_notifications')->default(true); + $table->boolean('server_disk_usage_pushover_notifications')->default(true); + $table->boolean('server_reachable_pushover_notifications')->default(false); + $table->boolean('server_unreachable_pushover_notifications')->default(true); + + $table->unique(['team_id']); + }); + $teams = DB::table('teams')->get(); + + foreach ($teams as $team) { + try { + DB::table('pushover_notification_settings')->insert([ + 'team_id' => $team->id, + ]); + } catch (\Throwable $e) { + Log::error('Error creating pushover notification settings for existing teams: '.$e->getMessage()); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('pushover_notification_settings'); + } +}; diff --git a/database/migrations/2024_12_11_161418_add_authentik_base_url_to_oauth_settings_table.php b/database/migrations/2024_12_11_161418_add_authentik_base_url_to_oauth_settings_table.php new file mode 100644 index 000000000..44ecb0cde --- /dev/null +++ b/database/migrations/2024_12_11_161418_add_authentik_base_url_to_oauth_settings_table.php @@ -0,0 +1,28 @@ +string('base_url')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('oauth_settings', function (Blueprint $table) { + $table->dropColumn('base_url'); + }); + } +}; diff --git a/database/migrations/2024_12_13_103007_encrypt_resend_api_key_in_instance_settings.php b/database/migrations/2024_12_13_103007_encrypt_resend_api_key_in_instance_settings.php new file mode 100644 index 000000000..ab9b3416a --- /dev/null +++ b/database/migrations/2024_12_13_103007_encrypt_resend_api_key_in_instance_settings.php @@ -0,0 +1,46 @@ +exists()) { + $settings = DB::table('instance_settings')->get(); + foreach ($settings as $setting) { + try { + DB::table('instance_settings')->where('id', $setting->id)->update([ + 'resend_api_key' => $setting->resend_api_key ? Crypt::encryptString($setting->resend_api_key) : null, + ]); + } catch (Exception $e) { + \Log::error('Error encrypting resend_api_key: '.$e->getMessage()); + } + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (DB::table('instance_settings')->exists()) { + $settings = DB::table('instance_settings')->get(); + foreach ($settings as $setting) { + try { + DB::table('instance_settings')->where('id', $setting->id)->update([ + 'resend_api_key' => $setting->resend_api_key ? Crypt::decryptString($setting->resend_api_key) : null, + ]); + } catch (Exception $e) { + \Log::error('Error decrypting resend_api_key: '.$e->getMessage()); + } + } + } + } +}; diff --git a/database/seeders/OauthSettingSeeder.php b/database/seeders/OauthSettingSeeder.php index 16abf9e04..df7619fec 100644 --- a/database/seeders/OauthSettingSeeder.php +++ b/database/seeders/OauthSettingSeeder.php @@ -4,6 +4,7 @@ namespace Database\Seeders; use App\Models\OauthSetting; use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Log; class OauthSettingSeeder extends Seeder { @@ -12,25 +13,53 @@ class OauthSettingSeeder extends Seeder */ public function run(): void { - OauthSetting::firstOrCreate([ - 'id' => 0, - 'provider' => 'azure', - ]); - OauthSetting::firstOrCreate([ - 'id' => 1, - 'provider' => 'bitbucket', - ]); - OauthSetting::firstOrCreate([ - 'id' => 2, - 'provider' => 'github', - ]); - OauthSetting::firstOrCreate([ - 'id' => 3, - 'provider' => 'gitlab', - ]); - OauthSetting::firstOrCreate([ - 'id' => 4, - 'provider' => 'google', - ]); + try { + $providers = collect([ + 'azure', + 'bitbucket', + 'github', + 'gitlab', + 'google', + 'authentik', + ]); + + $isOauthSeeded = OauthSetting::count() > 0; + + // We changed how providers are defined in the database, so we authentik does not exists, we need to recreate all of the auth providers + // Before authentik was a provider, providers started with 0 id + + $isOauthAuthentik = OauthSetting::where('provider', 'authentik')->exists(); + if (! $isOauthSeeded || $isOauthAuthentik) { + foreach ($providers as $provider) { + OauthSetting::updateOrCreate([ + 'provider' => $provider, + ]); + } + + return; + } + + $allProviders = OauthSetting::all(); + $notFoundProviders = $providers->diff($allProviders->pluck('provider')); + + $allProviders->each(function ($provider) { + $provider->delete(); + }); + $allProviders->each(function ($provider) { + $provider = new OauthSetting; + $provider->provider = $provider->provider; + unset($provider->id); + $provider->save(); + }); + + foreach ($notFoundProviders as $provider) { + OauthSetting::create([ + 'provider' => $provider, + ]); + } + + } catch (\Exception $e) { + Log::error($e->getMessage()); + } } } diff --git a/docker/development/etc/nginx/site-opts.d/http.conf b/docker/development/etc/nginx/site-opts.d/http.conf index 41735cf06..9f7e49ee2 100644 --- a/docker/development/etc/nginx/site-opts.d/http.conf +++ b/docker/development/etc/nginx/site-opts.d/http.conf @@ -39,7 +39,7 @@ location ~ \.php$ { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; - fastcgi_buffers 8 8k; - fastcgi_buffer_size 8k; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; fastcgi_read_timeout 99; } diff --git a/docker/development/etc/php/conf.d/zzz-custom-php.ini b/docker/development/etc/php/conf.d/zzz-custom-php.ini index 33fb865e0..dcf048904 100644 --- a/docker/development/etc/php/conf.d/zzz-custom-php.ini +++ b/docker/development/etc/php/conf.d/zzz-custom-php.ini @@ -7,3 +7,4 @@ ignore_repeated_source = On upload_max_filesize = 256M post_max_size = 256M +memory_limit = ${PHP_MEMORY_LIMIT:-256M} diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 09281a666..aaef29f9d 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -99,6 +99,9 @@ RUN mkdir -p /usr/local/bin && \ COPY docker/production/etc/php/conf.d/zzz-custom-php.ini /usr/local/etc/php/conf.d/zzz-custom-php.ini ENV PHP_OPCACHE_ENABLE=1 +# Configure entrypoint +COPY --chmod=755 docker/production/entrypoint.d/ /etc/entrypoint.d + # Copy application files from previous stages COPY --from=base --chown=www-data:www-data /var/www/html/vendor ./vendor COPY --from=static-assets --chown=www-data:www-data /app/public/build ./public/build diff --git a/docker/production/entrypoint.d/99-debug-mode.sh b/docker/production/entrypoint.d/99-debug-mode.sh new file mode 100644 index 000000000..e0a181373 --- /dev/null +++ b/docker/production/entrypoint.d/99-debug-mode.sh @@ -0,0 +1,8 @@ +# Debug mode +if [ "$APP_DEBUG" = "true" ]; then + echo "Debug mode is enabled" + echo "Installing development dependencies..." + composer install --dev --no-scripts + echo "Clearing optimized classes..." + php artisan optimize:clear +fi diff --git a/docker/production/etc/nginx/site-opts.d/http.conf b/docker/production/etc/nginx/site-opts.d/http.conf index 41735cf06..9f7e49ee2 100644 --- a/docker/production/etc/nginx/site-opts.d/http.conf +++ b/docker/production/etc/nginx/site-opts.d/http.conf @@ -39,7 +39,7 @@ location ~ \.php$ { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; - fastcgi_buffers 8 8k; - fastcgi_buffer_size 8k; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; fastcgi_read_timeout 99; } diff --git a/docker/production/etc/php/conf.d/zzz-custom-php.ini b/docker/production/etc/php/conf.d/zzz-custom-php.ini index a2297ad0d..ee18b77e9 100644 --- a/docker/production/etc/php/conf.d/zzz-custom-php.ini +++ b/docker/production/etc/php/conf.d/zzz-custom-php.ini @@ -7,3 +7,4 @@ ignore_repeated_source = On upload_max_filesize = 256M post_max_size = 256M +memory_limit = ${PHP_MEMORY_LIMIT:-256M} diff --git a/docker/production/etc/s6-overlay/s6-rc.d/db-migration/up b/docker/production/etc/s6-overlay/s6-rc.d/db-migration/up index f1a5006f1..c2548d136 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/db-migration/up +++ b/docker/production/etc/s6-overlay/s6-rc.d/db-migration/up @@ -6,8 +6,6 @@ cd /var/www/html foreground { php artisan - migrate - --force - --isolated + start:migration } diff --git a/docker/production/etc/s6-overlay/s6-rc.d/init-seeder/up b/docker/production/etc/s6-overlay/s6-rc.d/init-seeder/up index 20d17c58b..007f4233e 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/init-seeder/up +++ b/docker/production/etc/s6-overlay/s6-rc.d/init-seeder/up @@ -6,10 +6,7 @@ cd /var/www/html foreground { php artisan - db:seed - --class - ProductionSeeder - --force + start:seeder } diff --git a/lang/en.json b/lang/en.json index 5ea474b02..4e0749ece 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,5 +1,6 @@ { "auth.login": "Login", + "auth.login.authentik": "Login with Authentik", "auth.login.azure": "Login with Microsoft", "auth.login.bitbucket": "Login with Bitbucket", "auth.login.github": "Login with GitHub", diff --git a/other/logos/internetgarden.ico b/other/logos/internetgarden.ico new file mode 100644 index 000000000..e555984be Binary files /dev/null and b/other/logos/internetgarden.ico differ diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 4a03a5c98..def105aa7 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -13,6 +13,50 @@ DOCKER_VERSION="27.0" # TODO: Ask for a user CURRENT_USER=$USER +if [ $EUID != 0 ]; then + echo "Please run this script as root or with sudo" + exit +fi + +echo -e "Welcome to Coolify Installer!" +echo -e "This script will install everything for you. Sit back and relax." +echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n" + +TOTAL_SPACE=$(df -BG / | awk 'NR==2 {print $2}' | sed 's/G//') +AVAILABLE_SPACE=$(df -BG / | awk 'NR==2 {print $4}' | sed 's/G//') +REQUIRED_TOTAL_SPACE=30 +REQUIRED_AVAILABLE_SPACE=20 +WARNING_SPACE=false + +if [ "$TOTAL_SPACE" -lt "$REQUIRED_TOTAL_SPACE" ]; then + WARNING_SPACE=true + cat << EOF +WARNING: Insufficient total disk space! + +Total disk space: ${TOTAL_SPACE}GB +Required disk space: ${REQUIRED_TOTAL_SPACE}GB + +================== +EOF +fi + +if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_AVAILABLE_SPACE" ]; then + cat << EOF +WARNING: Insufficient available disk space! + +Available disk space: ${AVAILABLE_SPACE}GB +Required available space: ${REQUIRED_AVAILABLE_SPACE}GB + +================== +EOF +WARNING_SPACE=true +fi + +if [ "$WARNING_SPACE" = true ]; then + echo "Sleeping for 5 seconds." + sleep 5 +fi + mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel} mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/proxy/dynamic @@ -39,6 +83,11 @@ if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then OS_TYPE="arch" fi +# Check if the OS is Endeavour OS, if so, change it to arch +if [ "$OS_TYPE" = "endeavouros" ]; then + OS_TYPE="arch" +fi + # Check if the OS is Asahi Linux, if so, change it to fedora if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then OS_TYPE="fedora" @@ -83,11 +132,6 @@ if [ -z "$LATEST_REALTIME_VERSION" ]; then fi -if [ $EUID != 0 ]; then - echo "Please run as root" - exit -fi - case "$OS_TYPE" in arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;; *) @@ -103,21 +147,8 @@ if [ "$1" != "" ]; then LATEST_VERSION="${LATEST_VERSION#v}" fi -echo -e "\033[0;35m" -cat << "EOF" - _____ _ _ __ - / ____| | (_)/ _| - | | ___ ___ | |_| |_ _ _ - | | / _ \ / _ \| | | _| | | | - | |___| (_) | (_) | | | | | |_| | - \_____\___/ \___/|_|_|_| \__, | - __/ | - |___/ -EOF -echo -e "\033[0m" -echo -e "Welcome to Coolify Installer!" -echo -e "This script will install everything for you. Sit back and relax." -echo -e "Source code: https://github.com/coollabsio/coolify/blob/main/scripts/install.sh\n" + + echo -e "---------------------------------------------" echo "| Operating System | $OS_TYPE $OS_VERSION" echo "| Docker | $DOCKER_VERSION" @@ -125,24 +156,24 @@ echo "| Coolify | $LATEST_VERSION" echo "| Helper | $LATEST_HELPER_VERSION" echo "| Realtime | $LATEST_REALTIME_VERSION" echo -e "---------------------------------------------\n" -echo -e "1. Installing required packages (curl, wget, git, jq). " +echo -e "1. Installing required packages (curl, wget, git, jq, openssl). " case "$OS_TYPE" in arch) - pacman -Sy --noconfirm --needed curl wget git jq >/dev/null || true + pacman -Sy --noconfirm --needed curl wget git jq openssl >/dev/null || true ;; alpine) sed -i '/^#.*\/community/s/^#//' /etc/apk/repositories apk update >/dev/null - apk add curl wget git jq >/dev/null + apk add curl wget git jq openssl >/dev/null ;; ubuntu | debian | raspbian) apt-get update -y >/dev/null - apt-get install -y curl wget git jq >/dev/null + apt-get install -y curl wget git jq openssl >/dev/null ;; centos | fedora | rhel | ol | rocky | almalinux | amzn) if [ "$OS_TYPE" = "amzn" ]; then - dnf install -y wget git jq >/dev/null + dnf install -y wget git jq openssl >/dev/null else if ! command -v dnf >/dev/null; then yum install -y dnf >/dev/null @@ -150,12 +181,12 @@ centos | fedora | rhel | ol | rocky | almalinux | amzn) if ! command -v curl >/dev/null; then dnf install -y curl >/dev/null fi - dnf install -y wget git jq >/dev/null + dnf install -y wget git jq openssl >/dev/null fi ;; sles | opensuse-leap | opensuse-tumbleweed) zypper refresh >/dev/null - zypper install -y curl wget git jq >/dev/null + zypper install -y curl wget git jq openssl >/dev/null ;; *) echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." @@ -300,6 +331,22 @@ if ! [ -x "$(command -v docker)" ]; then exit 1 fi ;; + "fedora") + if [ -x "$(command -v dnf5)" ]; then + # dnf5 is available + dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo --overwrite >/dev/null 2>&1 + else + # dnf5 is not available, use dnf + dnf config-manager --add-repo=https://download.docker.com/linux/fedora/docker-ce.repo >/dev/null 2>&1 + fi + dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin >/dev/null 2>&1 + if ! [ -x "$(command -v docker)" ]; then + echo " - Docker could not be installed automatically. Please visit https://docs.docker.com/engine/install/ and install Docker manually to continue." + exit 1 + fi + systemctl start docker >/dev/null 2>&1 + systemctl enable docker >/dev/null 2>&1 + ;; *) if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)." @@ -490,7 +537,21 @@ echo -e "\033[0;35m \____\___/|_| |_|\__, |_| \__,_|\__|\__,_|_|\__,_|\__|_|\___/|_| |_|___(_) |___/ \033[0m" -echo -e "\nYour instance is ready to use." -echo -e "Please visit http://$(curl -4s https://ifconfig.io):8000 to get started.\n" -echo -e "WARNING: We recommend you to backup your /data/coolify/source/.env file to a safe location, outside of this server." +echo -e "\nYour instance is ready to use!\n" +echo -e "You can access Coolify through your Public IP: http://$(curl -4s https://ifconfig.io):8000" + +set +e +DEFAULT_PRIVATE_IP=$(ip route get 1 | sed -n 's/^.*src \([0-9.]*\) .*$/\1/p') +PRIVATE_IPS=$(hostname -I) +set -e + +if [ -n "$PRIVATE_IPS" ]; then + echo -e "\nIf your Public IP is not accessible, you can use the following Private IPs:\n" + for IP in $PRIVATE_IPS; do + if [ "$IP" != "$DEFAULT_PRIVATE_IP" ]; then + echo -e "http://$IP:8000" + fi + done +fi +echo -e "\nWARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).\n" cp /data/coolify/source/.env /data/coolify/source/.env.backup diff --git a/other/nightly/versions.json b/other/nightly/versions.json index 8b10875d0..2d96743f4 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,16 +1,16 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.367" + "version": "4.0.0-beta.378" }, "nightly": { - "version": "4.0.0-beta.368" + "version": "4.0.0-beta.379" }, "helper": { - "version": "1.0.3" + "version": "1.0.4" }, "realtime": { - "version": "1.0.4" + "version": "1.0.5" }, "sentinel": { "version": "0.0.15" diff --git a/public/svgs/checkmate.png b/public/svgs/checkmate.png new file mode 100644 index 000000000..f0f31466b Binary files /dev/null and b/public/svgs/checkmate.png differ diff --git a/public/svgs/documenso.png b/public/svgs/documenso.png new file mode 100644 index 000000000..baa636532 Binary files /dev/null and b/public/svgs/documenso.png differ diff --git a/public/svgs/dolibarr.png b/public/svgs/dolibarr.png new file mode 100644 index 000000000..f09119afc Binary files /dev/null and b/public/svgs/dolibarr.png differ diff --git a/public/svgs/nexus.png b/public/svgs/nexus.png new file mode 100644 index 000000000..823ea4e86 Binary files /dev/null and b/public/svgs/nexus.png differ diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index d6c3edf84..9404ed2c5 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -12,7 +12,7 @@ @endforeach @endif -
+
@csrf @env('local') diff --git a/resources/views/components/forms/monaco-editor.blade.php b/resources/views/components/forms/monaco-editor.blade.php index d3793785b..a4bc88051 100644 --- a/resources/views/components/forms/monaco-editor.blade.php +++ b/resources/views/components/forms/monaco-editor.blade.php @@ -7,6 +7,9 @@ monacoLoader: true, monacoFontSize: '15px', monacoId: $id('monaco-editor'), + isDarkMode() { + return document.documentElement.classList.contains('dark') || localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches); + }, monacoEditor(editor) { editor.onDidChangeModelContent((e) => { this.monacoContent = editor.getValue(); @@ -41,357 +44,9 @@ let proxy = URL.createObjectURL(new Blob([` self.MonacoEnvironment = { baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min' }; importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.39.0/min/vs/base/worker/workerMain.min.js');`], { type: 'text/javascript' })); window.MonacoEnvironment = { getWorkerUrl: () => proxy }; require(['vs/editor/editor.main'], () => { - monaco.editor.defineTheme('blackboard', { - 'base': 'vs-dark', - 'inherit': true, - 'rules': [{ - 'background': editorBackground, - 'token': '' - }, - { - 'foreground': '959da5', - 'token': 'comment' - }, - { - 'foreground': '959da5', - 'token': 'punctuation.definition.comment' - }, - { - 'foreground': '959da5', - 'token': 'string.comment' - }, - { - 'foreground': 'c8e1ff', - 'token': 'constant' - }, - { - 'foreground': 'c8e1ff', - 'token': 'entity.name.constant' - }, - { - 'foreground': 'c8e1ff', - 'token': 'variable.other.constant' - }, - { - 'foreground': 'c8e1ff', - 'token': 'variable.language' - }, - { - 'foreground': 'b392f0', - 'token': 'entity' - }, - { - 'foreground': 'b392f0', - 'token': 'entity.name' - }, - { - 'foreground': 'f6f8fa', - 'token': 'variable.parameter.function' - }, - { - 'foreground': '7bcc72', - 'token': 'entity.name.tag' - }, - { - 'foreground': 'ea4a5a', - 'token': 'keyword' - }, - { - 'foreground': 'ea4a5a', - 'token': 'storage' - }, - { - 'foreground': 'ea4a5a', - 'token': 'storage.type' - }, - { - 'foreground': 'f6f8fa', - 'token': 'storage.modifier.package' - }, - { - 'foreground': 'f6f8fa', - 'token': 'storage.modifier.import' - }, - { - 'foreground': 'f6f8fa', - 'token': 'storage.type.java' - }, - { - 'foreground': '79b8ff', - 'token': 'string' - }, - { - 'foreground': '79b8ff', - 'token': 'punctuation.definition.string' - }, - { - 'foreground': '79b8ff', - 'token': 'string punctuation.section.embedded source' - }, - { - 'foreground': 'c8e1ff', - 'token': 'support' - }, - { - 'foreground': 'c8e1ff', - 'token': 'meta.property-name' - }, - { - 'foreground': 'fb8532', - 'token': 'variable' - }, - { - 'foreground': 'f6f8fa', - 'token': 'variable.other' - }, - { - 'foreground': 'd73a49', - 'fontStyle': 'bold italic underline', - 'token': 'invalid.broken' - }, - { - 'foreground': 'd73a49', - 'fontStyle': 'bold italic underline', - 'token': 'invalid.deprecated' - }, - { - 'foreground': 'fafbfc', - 'background': 'd73a49', - 'fontStyle': 'italic underline', - 'token': 'invalid.illegal' - }, - { - 'foreground': 'fafbfc', - 'background': 'd73a49', - 'fontStyle': 'italic underline', - 'token': 'carriage-return' - }, - { - 'foreground': 'd73a49', - 'fontStyle': 'bold italic underline', - 'token': 'invalid.unimplemented' - }, - { - 'foreground': 'd73a49', - 'token': 'message.error' - }, - { - 'foreground': 'f6f8fa', - 'token': 'string source' - }, - { - 'foreground': 'c8e1ff', - 'token': 'string variable' - }, - { - 'foreground': '79b8ff', - 'token': 'source.regexp' - }, - { - 'foreground': '79b8ff', - 'token': 'string.regexp' - }, - { - 'foreground': '79b8ff', - 'token': 'string.regexp.character-class' - }, - { - 'foreground': '79b8ff', - 'token': 'string.regexp constant.character.escape' - }, - { - 'foreground': '79b8ff', - 'token': 'string.regexp source.ruby.embedded' - }, - { - 'foreground': '79b8ff', - 'token': 'string.regexp string.regexp.arbitrary-repitition' - }, - { - 'foreground': '7bcc72', - 'fontStyle': 'bold', - 'token': 'string.regexp constant.character.escape' - }, - { - 'foreground': 'c8e1ff', - 'token': 'support.constant' - }, - { - 'foreground': 'c8e1ff', - 'token': 'support.variable' - }, - { - 'foreground': 'c8e1ff', - 'token': 'meta.module-reference' - }, - { - 'foreground': 'fb8532', - 'token': 'markup.list' - }, - { - 'foreground': '0366d6', - 'fontStyle': 'bold', - 'token': 'markup.heading' - }, - { - 'foreground': '0366d6', - 'fontStyle': 'bold', - 'token': 'markup.heading entity.name' - }, - { - 'foreground': 'c8e1ff', - 'token': 'markup.quote' - }, - { - 'foreground': 'f6f8fa', - 'fontStyle': 'italic', - 'token': 'markup.italic' - }, - { - 'foreground': 'f6f8fa', - 'fontStyle': 'bold', - 'token': 'markup.bold' - }, - { - 'foreground': 'c8e1ff', - 'token': 'markup.raw' - }, - { - 'foreground': 'b31d28', - 'background': 'ffeef0', - 'token': 'markup.deleted' - }, - { - 'foreground': 'b31d28', - 'background': 'ffeef0', - 'token': 'meta.diff.header.from-file' - }, - { - 'foreground': 'b31d28', - 'background': 'ffeef0', - 'token': 'punctuation.definition.deleted' - }, - { - 'foreground': '176f2c', - 'background': 'f0fff4', - 'token': 'markup.inserted' - }, - { - 'foreground': '176f2c', - 'background': 'f0fff4', - 'token': 'meta.diff.header.to-file' - }, - { - 'foreground': '176f2c', - 'background': 'f0fff4', - 'token': 'punctuation.definition.inserted' - }, - { - 'foreground': 'b08800', - 'background': 'fffdef', - 'token': 'markup.changed' - }, - { - 'foreground': 'b08800', - 'background': 'fffdef', - 'token': 'punctuation.definition.changed' - }, - { - 'foreground': '2f363d', - 'background': '959da5', - 'token': 'markup.ignored' - }, - { - 'foreground': '2f363d', - 'background': '959da5', - 'token': 'markup.untracked' - }, - { - 'foreground': 'b392f0', - 'fontStyle': 'bold', - 'token': 'meta.diff.range' - }, - { - 'foreground': 'c8e1ff', - 'token': 'meta.diff.header' - }, - { - 'foreground': '0366d6', - 'fontStyle': 'bold', - 'token': 'meta.separator' - }, - { - 'foreground': '0366d6', - 'token': 'meta.output' - }, - { - 'foreground': 'ffeef0', - 'token': 'brackethighlighter.tag' - }, - { - 'foreground': 'ffeef0', - 'token': 'brackethighlighter.curly' - }, - { - 'foreground': 'ffeef0', - 'token': 'brackethighlighter.round' - }, - { - 'foreground': 'ffeef0', - 'token': 'brackethighlighter.square' - }, - { - 'foreground': 'ffeef0', - 'token': 'brackethighlighter.angle' - }, - { - 'foreground': 'ffeef0', - 'token': 'brackethighlighter.quote' - }, - { - 'foreground': 'd73a49', - 'token': 'brackethighlighter.unmatched' - }, - { - 'foreground': 'd73a49', - 'token': 'sublimelinter.mark.error' - }, - { - 'foreground': 'fb8532', - 'token': 'sublimelinter.mark.warning' - }, - { - 'foreground': '6a737d', - 'token': 'sublimelinter.gutter-mark' - }, - { - 'foreground': '79b8ff', - 'fontStyle': 'underline', - 'token': 'constant.other.reference.link' - }, - { - 'foreground': '79b8ff', - 'fontStyle': 'underline', - 'token': 'string.other.link' - } - ], - 'colors': { - 'editor.foreground': '#f6f8fa', - 'editor.background': editorBackground, - 'editor.selectionBackground': '#4c2889', - 'editor.inactiveSelectionBackground': '#444d56', - 'editor.lineHighlightBackground': '#444d56', - 'editorCursor.foreground': '#ffffff', - 'editorWhitespace.foreground': '#6a737d', - 'editorIndentGuide.background': '#6a737d', - 'editorIndentGuide.activeBackground': '#f6f8fa', - 'editor.selectionHighlightBorder': '#444d56' - } - }); - const editor = monaco.editor.create($refs.monacoEditorElement, { value: monacoContent, - theme: editorTheme, + theme: document.documentElement.classList.contains('dark') ? 'vs-dark' : 'vs', wordWrap: 'on', readOnly: '{{ $readonly ?? false }}', minimap: { enabled: false }, @@ -399,7 +54,20 @@ lineNumbersMinChars: 3, automaticLayout: true, language: '{{ $language }}' + }); + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'class') { + const isDark = document.documentElement.classList.contains('dark'); + monaco.editor.setTheme(isDark ? 'vs-dark' : 'vs'); + } + }); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] }); monacoEditor(editor); @@ -411,7 +79,6 @@ updatePlaceholder(editor.getValue()); - // Watch for changes in monacoContent from Livewire $watch('monacoContent', value => { if (editor.getValue() !== value) { editor.setValue(value); diff --git a/resources/views/components/modal-confirmation.blade.php b/resources/views/components/modal-confirmation.blade.php index 8495417ac..e98a494c5 100644 --- a/resources/views/components/modal-confirmation.blade.php +++ b/resources/views/components/modal-confirmation.blade.php @@ -258,6 +258,7 @@