diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php
index 51303d87a..6c8dd5234 100644
--- a/app/Actions/Proxy/CheckProxy.php
+++ b/app/Actions/Proxy/CheckProxy.php
@@ -30,7 +30,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
- ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
+ ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
if (! $uptime) {
throw new \Exception($error);
}
diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php
index 53c443778..be9b4062c 100644
--- a/app/Actions/Server/UpdateCoolify.php
+++ b/app/Actions/Server/UpdateCoolify.php
@@ -31,7 +31,7 @@ class UpdateCoolify
}
CleanupDocker::dispatch($this->server);
$this->latestVersion = get_latest_version_of_coolify();
- $this->currentVersion = config('version');
+ $this->currentVersion = config('constants.coolify.version');
if (! $manual_update) {
if (! $settings->is_auto_update_enabled) {
return;
diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php
index 57bbe896b..216262819 100644
--- a/app/Console/Commands/Init.php
+++ b/app/Console/Commands/Init.php
@@ -200,7 +200,7 @@ class Init extends Command
private function restore_coolify_db_backup()
{
- if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
+ if (version_compare('4.0.0-beta.179', config('constants.coolify.version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -228,7 +228,7 @@ class Init extends Command
private function send_alive_signal()
{
$id = config('app.id');
- $version = config('version');
+ $version = config('constants.coolify.version');
$settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
@@ -264,7 +264,7 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
- if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
+ if (version_compare('4.0.0-beta.298', config('constants.coolify.version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index e113dbe9a..832dcf58b 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -50,7 +50,7 @@ class Kernel extends ConsoleKernel
$this->instanceTimezone = config('app.timezone');
}
- $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
+ // $this->scheduleInstance->job(new CleanupStaleMultiplexedConnections)->hourly();
if (isDev()) {
// Instance Jobs
@@ -154,7 +154,7 @@ class Kernel extends ConsoleKernel
}
// Cleanup multiplexed connections every hour
- $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
+ // $this->scheduleInstance->job(new ServerCleanupMux($server))->hourly()->onOneServer();
// Temporary solution until we have better memory management for Sentinel
if ($server->isSentinelEnabled()) {
diff --git a/app/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php
index b457dc6a0..96b35a5ca 100644
--- a/app/Events/DatabaseProxyStopped.php
+++ b/app/Events/DatabaseProxyStopped.php
@@ -18,7 +18,7 @@ class DatabaseProxyStopped implements ShouldBroadcast
public function __construct($teamId = null)
{
if (is_null($teamId)) {
- $teamId = Auth::user()->currentTeam()->id ?? null;
+ $teamId = Auth::user()?->currentTeam()?->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php
index b35c72116..303e6535d 100644
--- a/app/Http/Controllers/Api/OtherController.php
+++ b/app/Http/Controllers/Api/OtherController.php
@@ -37,7 +37,7 @@ class OtherController extends Controller
)]
public function version(Request $request)
{
- return response(config('version'));
+ return response(config('constants.coolify.version'));
}
#[OA\Get(
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index 3f0f4d2c3..8c13b1a01 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -567,6 +567,9 @@ class ServersController extends Controller
['bearerAuth' => []],
],
tags: ['Servers'],
+ parameters: [
+ new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
+ ],
requestBody: new OA\RequestBody(
required: true,
description: 'Server updated.',
@@ -596,8 +599,7 @@ class ServersController extends Controller
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
- type: 'array',
- items: new OA\Items(ref: '#/components/schemas/Server')
+ ref: '#/components/schemas/Server'
)
),
]),
@@ -679,7 +681,7 @@ class ServersController extends Controller
}
return response()->json([
-
+ 'uuid' => $server->uuid,
])->setStatusCode(201);
}
diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php
index e94209b23..83ba16699 100644
--- a/app/Http/Controllers/Webhook/Stripe.php
+++ b/app/Http/Controllers/Webhook/Stripe.php
@@ -3,21 +3,26 @@
namespace App\Http\Controllers\Webhook;
use App\Http\Controllers\Controller;
-use App\Jobs\ServerLimitCheckJob;
-use App\Jobs\SubscriptionInvoiceFailedJob;
-use App\Models\Subscription;
-use App\Models\Team;
+use App\Jobs\StripeProcessJob;
use App\Models\Webhook;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
class Stripe extends Controller
{
+ protected $webhook;
+
public function events(Request $request)
{
try {
+ $webhookSecret = config('subscription.stripe_webhook_secret');
+ $signature = $request->header('Stripe-Signature');
+ $event = \Stripe\Webhook::constructEvent(
+ $request->getContent(),
+ $signature,
+ $webhookSecret
+ );
if (app()->isDownForMaintenance()) {
$epoch = now()->valueOf();
$data = [
@@ -33,241 +38,17 @@ class Stripe extends Controller
$json = json_encode($data);
Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json);
- return;
+ return response('Webhook received. Cool cool cool cool cool.', 200);
}
- $webhookSecret = config('subscription.stripe_webhook_secret');
- $signature = $request->header('Stripe-Signature');
- $excludedPlans = config('subscription.stripe_excluded_plans');
- $event = \Stripe\Webhook::constructEvent(
- $request->getContent(),
- $signature,
- $webhookSecret
- );
- $webhook = Webhook::create([
+ $this->webhook = Webhook::create([
'type' => 'stripe',
'payload' => $request->getContent(),
]);
- $type = data_get($event, 'type');
- $data = data_get($event, 'data.object');
- switch ($type) {
- case 'radar.early_fraud_warning.created':
- $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
- $id = data_get($data, 'id');
- $charge = data_get($data, 'charge');
- if ($charge) {
- $stripe->refunds->create(['charge' => $charge]);
- }
- $pi = data_get($data, 'payment_intent');
- $piData = $stripe->paymentIntents->retrieve($pi, []);
- $customerId = data_get($piData, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- $subscriptionId = data_get($subscription, 'stripe_subscription_id');
- $stripe->subscriptions->cancel($subscriptionId, []);
- $subscription->update([
- 'stripe_invoice_paid' => false,
- ]);
- send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
- } else {
- send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ StripeProcessJob::dispatch($event);
- return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400);
- }
- break;
- case 'checkout.session.completed':
- $clientReferenceId = data_get($data, 'client_reference_id');
- if (is_null($clientReferenceId)) {
- send_internal_notification('Checkout session completed without client reference id.');
- break;
- }
- $userId = Str::before($clientReferenceId, ':');
- $teamId = Str::after($clientReferenceId, ':');
- $subscriptionId = data_get($data, 'subscription');
- $customerId = data_get($data, 'customer');
- $team = Team::find($teamId);
- $found = $team->members->where('id', $userId)->first();
- if (! $found->isAdmin()) {
- send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
-
- return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400);
- }
- $subscription = Subscription::where('team_id', $teamId)->first();
- if ($subscription) {
- // send_internal_notification('Old subscription activated for team: '.$teamId);
- $subscription->update([
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => true,
- ]);
- } else {
- // send_internal_notification('New subscription for team: '.$teamId);
- Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => true,
- ]);
- }
- break;
- case 'invoice.paid':
- $customerId = data_get($data, 'customer');
- $planId = data_get($data, 'lines.data.0.plan.id');
- if (Str::contains($excludedPlans, $planId)) {
- // send_internal_notification('Subscription excluded.');
- break;
- }
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- $subscription->update([
- 'stripe_invoice_paid' => true,
- ]);
- } else {
- return response("No subscription found for customer: {$customerId}", 400);
- }
- break;
- case 'invoice.payment_failed':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- // send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
-
- return response('No subscription found in Coolify.');
- }
- $team = data_get($subscription, 'team');
- if (! $team) {
- // send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
-
- return response('No team found in Coolify.');
- }
- if (! $subscription->stripe_invoice_paid) {
- SubscriptionInvoiceFailedJob::dispatch($team);
- // send_internal_notification('Invoice payment failed: '.$customerId);
- } else {
- // send_internal_notification('Invoice payment failed but already paid: '.$customerId);
- }
- break;
- case 'payment_intent.payment_failed':
- $customerId = data_get($data, 'customer');
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- // send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
-
- return response('No subscription found in Coolify.');
- }
- if ($subscription->stripe_invoice_paid) {
- // send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
-
- return;
- }
- send_internal_notification('Subscription payment failed for customer: '.$customerId);
- break;
- case 'customer.subscription.created':
- $customerId = data_get($data, 'customer');
- $subscriptionId = data_get($data, 'id');
- $teamId = data_get($data, 'metadata.team_id');
- $userId = data_get($data, 'metadata.user_id');
- if (! $teamId || ! $userId) {
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if ($subscription) {
- return response("Subscription already exists for customer: {$customerId}", 200);
- }
-
- return response('No team id or user id found', 400);
- }
- $team = Team::find($teamId);
- $found = $team->members->where('id', $userId)->first();
- if (! $found->isAdmin()) {
- send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
-
- return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400);
- }
- $subscription = Subscription::where('team_id', $teamId)->first();
- if ($subscription) {
- return response("Subscription already exists for team: {$teamId}", 200);
- } else {
- Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => false,
- ]);
-
- return response('Subscription created');
- }
- case 'customer.subscription.updated':
- $teamId = data_get($data, 'metadata.team_id');
- $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');
- if (Str::contains($excludedPlans, $planId)) {
- // send_internal_notification('Subscription excluded.');
- break;
- }
- $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
- if (! $subscription) {
- if ($status === 'incomplete_expired') {
- return response('Subscription incomplete expired', 200);
- }
- if ($teamId) {
- $subscription = Subscription::create([
- 'team_id' => $teamId,
- 'stripe_subscription_id' => $subscriptionId,
- 'stripe_customer_id' => $customerId,
- 'stripe_invoice_paid' => false,
- ]);
- } else {
- return response('No subscription and team id found', 400);
- }
- }
- $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
- $feedback = data_get($data, 'cancellation_details.feedback');
- $comment = data_get($data, 'cancellation_details.comment');
- $lookup_key = data_get($data, 'items.data.0.price.lookup_key');
- if (str($lookup_key)->contains('dynamic')) {
- $quantity = data_get($data, 'items.data.0.quantity', 2);
- $team = data_get($subscription, 'team');
- if ($team) {
- $team->update([
- 'custom_server_limit' => $quantity,
- ]);
- }
- ServerLimitCheckJob::dispatch($team);
- }
- $subscription->update([
- 'stripe_feedback' => $feedback,
- 'stripe_comment' => $comment,
- 'stripe_plan_id' => $planId,
- 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
- ]);
- if ($status === 'paused' || $status === 'incomplete_expired') {
- $subscription->update([
- 'stripe_invoice_paid' => false,
- ]);
- }
- if ($feedback) {
- $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
- if ($comment) {
- $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();
- break;
- default:
- // Unhandled event type
- }
+ return response('Webhook received. Cool cool cool cool cool.', 200);
} catch (Exception $e) {
- if ($type !== 'payment_intent.payment_failed') {
- send_internal_notification("Subscription webhook ($type) failed: ".$e->getMessage());
- }
- $webhook->update([
+ $this->webhook->update([
'status' => 'failed',
'failure_reason' => $e->getMessage(),
]);
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 270243eaf..6a66bb56d 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -463,7 +463,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
$this->save_environment_variables();
if (! is_null($this->env_filename)) {
- $services = collect($composeFile['services']);
+ $services = collect(data_get($composeFile, 'services', []));
$services = $services->map(function ($service, $name) {
$service['env_file'] = [$this->env_filename];
diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php
index f2348118a..1d3a345e1 100644
--- a/app/Jobs/CheckForUpdatesJob.php
+++ b/app/Jobs/CheckForUpdatesJob.php
@@ -27,7 +27,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
$versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version');
- $current_version = config('version');
+ $current_version = config('constants.coolify.version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available
diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php
index 5c6aa26b3..89674b255 100644
--- a/app/Jobs/DatabaseBackupJob.php
+++ b/app/Jobs/DatabaseBackupJob.php
@@ -67,6 +67,8 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
+ $databasesToBackup = null;
+
$this->team = Team::find($this->backup->team_id);
if (! $this->team) {
$this->backup->delete();
@@ -198,8 +200,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
}
-
- if (filled($databasesToBackup)) {
+ if (blank($databasesToBackup)) {
if (str($databaseType)->contains('postgres')) {
$databasesToBackup = [$this->database->postgres_db];
} elseif (str($databaseType)->contains('mongodb')) {
diff --git a/app/Jobs/SendMessageToTelegramJob.php b/app/Jobs/SendMessageToTelegramJob.php
index 6c581e1d3..85f4fc934 100644
--- a/app/Jobs/SendMessageToTelegramJob.php
+++ b/app/Jobs/SendMessageToTelegramJob.php
@@ -72,7 +72,7 @@ class SendMessageToTelegramJob implements ShouldBeEncrypted, ShouldQueue
}
$response = Http::post($url, $payload);
if ($response->failed()) {
- throw new \Exception('Telegram notification failed with '.$response->status().' status code.'.$response->body());
+ throw new \RuntimeException('Telegram notification failed with '.$response->status().' status code.'.$response->body());
}
}
}
diff --git a/app/Jobs/StripeProcessJob.php b/app/Jobs/StripeProcessJob.php
new file mode 100644
index 000000000..00c9b6d18
--- /dev/null
+++ b/app/Jobs/StripeProcessJob.php
@@ -0,0 +1,246 @@
+onQueue('high');
+ }
+
+ public function handle(): void
+ {
+ try {
+ $excludedPlans = config('subscription.stripe_excluded_plans');
+
+ $type = data_get($this->event, 'type');
+ $this->type = $type;
+ $data = data_get($this->event, 'data.object');
+ switch ($type) {
+ case 'radar.early_fraud_warning.created':
+ $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
+ $id = data_get($data, 'id');
+ $charge = data_get($data, 'charge');
+ if ($charge) {
+ $stripe->refunds->create(['charge' => $charge]);
+ }
+ $pi = data_get($data, 'payment_intent');
+ $piData = $stripe->paymentIntents->retrieve($pi, []);
+ $customerId = data_get($piData, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ $subscriptionId = data_get($subscription, 'stripe_subscription_id');
+ $stripe->subscriptions->cancel($subscriptionId, []);
+ $subscription->update([
+ 'stripe_invoice_paid' => false,
+ ]);
+ send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ } else {
+ send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ throw new \RuntimeException("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}");
+ }
+ break;
+ case 'checkout.session.completed':
+ $clientReferenceId = data_get($data, 'client_reference_id');
+ if (is_null($clientReferenceId)) {
+ send_internal_notification('Checkout session completed without client reference id.');
+ break;
+ }
+ $userId = Str::before($clientReferenceId, ':');
+ $teamId = Str::after($clientReferenceId, ':');
+ $subscriptionId = data_get($data, 'subscription');
+ $customerId = data_get($data, 'customer');
+ $team = Team::find($teamId);
+ $found = $team->members->where('id', $userId)->first();
+ if (! $found->isAdmin()) {
+ send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
+ throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.");
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ send_internal_notification('Old subscription activated for team: '.$teamId);
+ $subscription->update([
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => true,
+ ]);
+ } else {
+ send_internal_notification('New subscription for team: '.$teamId);
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => true,
+ ]);
+ }
+ break;
+ case 'invoice.paid':
+ $customerId = data_get($data, 'customer');
+ $planId = data_get($data, 'lines.data.0.plan.id');
+ if (Str::contains($excludedPlans, $planId)) {
+ send_internal_notification('Subscription excluded.');
+ break;
+ }
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ $subscription->update([
+ 'stripe_invoice_paid' => true,
+ ]);
+ } else {
+ throw new \RuntimeException("No subscription found for customer: {$customerId}");
+ }
+ break;
+ case 'invoice.payment_failed':
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ send_internal_notification('invoice.payment_failed failed but no subscription found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No subscription found for customer: {$customerId}");
+ }
+ $team = data_get($subscription, 'team');
+ if (! $team) {
+ send_internal_notification('invoice.payment_failed failed but no team found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No team found in Coolify for customer: {$customerId}");
+ }
+ if (! $subscription->stripe_invoice_paid) {
+ SubscriptionInvoiceFailedJob::dispatch($team);
+ send_internal_notification('Invoice payment failed: '.$customerId);
+ } else {
+ send_internal_notification('Invoice payment failed but already paid: '.$customerId);
+ }
+ break;
+ case 'payment_intent.payment_failed':
+ $customerId = data_get($data, 'customer');
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ send_internal_notification('payment_intent.payment_failed, no subscription found in Coolify for customer: '.$customerId);
+ throw new \RuntimeException("No subscription found in Coolify for customer: {$customerId}");
+ }
+ if ($subscription->stripe_invoice_paid) {
+ send_internal_notification('payment_intent.payment_failed but invoice is active for customer: '.$customerId);
+
+ return;
+ }
+ send_internal_notification('Subscription payment failed for customer: '.$customerId);
+ break;
+ case 'customer.subscription.created':
+ $customerId = data_get($data, 'customer');
+ $subscriptionId = data_get($data, 'id');
+ $teamId = data_get($data, 'metadata.team_id');
+ $userId = data_get($data, 'metadata.user_id');
+ if (! $teamId || ! $userId) {
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if ($subscription) {
+ throw new \RuntimeException("Subscription already exists for customer: {$customerId}");
+ }
+ throw new \RuntimeException('No team id or user id found');
+ }
+ $team = Team::find($teamId);
+ $found = $team->members->where('id', $userId)->first();
+ if (! $found->isAdmin()) {
+ send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
+ throw new \RuntimeException("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.");
+ }
+ $subscription = Subscription::where('team_id', $teamId)->first();
+ if ($subscription) {
+ send_internal_notification("Subscription already exists for team: {$teamId}");
+ throw new \RuntimeException("Subscription already exists for team: {$teamId}");
+ } else {
+ Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ }
+ case 'customer.subscription.updated':
+ $teamId = data_get($data, 'metadata.team_id');
+ $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');
+ if (Str::contains($excludedPlans, $planId)) {
+ send_internal_notification('Subscription excluded.');
+ break;
+ }
+ $subscription = Subscription::where('stripe_customer_id', $customerId)->first();
+ if (! $subscription) {
+ if ($status === 'incomplete_expired') {
+ send_internal_notification('Subscription incomplete expired');
+ throw new \RuntimeException('Subscription incomplete expired');
+ }
+ if ($teamId) {
+ $subscription = Subscription::create([
+ 'team_id' => $teamId,
+ 'stripe_subscription_id' => $subscriptionId,
+ 'stripe_customer_id' => $customerId,
+ 'stripe_invoice_paid' => false,
+ ]);
+ } else {
+ send_internal_notification('No subscription and team id found');
+ throw new \RuntimeException('No subscription and team id found');
+ }
+ }
+ $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end');
+ $feedback = data_get($data, 'cancellation_details.feedback');
+ $comment = data_get($data, 'cancellation_details.comment');
+ $lookup_key = data_get($data, 'items.data.0.price.lookup_key');
+ if (str($lookup_key)->contains('dynamic')) {
+ $quantity = data_get($data, 'items.data.0.quantity', 2);
+ $team = data_get($subscription, 'team');
+ if ($team) {
+ $team->update([
+ 'custom_server_limit' => $quantity,
+ ]);
+ }
+ ServerLimitCheckJob::dispatch($team);
+ }
+ $subscription->update([
+ 'stripe_feedback' => $feedback,
+ 'stripe_comment' => $comment,
+ 'stripe_plan_id' => $planId,
+ 'stripe_cancel_at_period_end' => $cancelAtPeriodEnd,
+ ]);
+ if ($status === 'paused' || $status === 'incomplete_expired') {
+ $subscription->update([
+ 'stripe_invoice_paid' => false,
+ ]);
+ }
+ if ($feedback) {
+ $reason = "Cancellation feedback for {$customerId}: '".$feedback."'";
+ if ($comment) {
+ $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();
+ break;
+ default:
+ throw new \RuntimeException("Unhandled event type: {$type}");
+ }
+ } catch (\Exception $e) {
+ send_internal_notification('StripeProcessJob error: '.$e->getMessage());
+ }
+ }
+}
diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php
index f86f42e34..337f1d067 100644
--- a/app/Livewire/Destination/New/Docker.php
+++ b/app/Livewire/Destination/New/Docker.php
@@ -35,7 +35,7 @@ class Docker extends Component
$this->network = new Cuid2;
$this->servers = Server::isUsable()->get();
if ($server_id) {
- $this->selectedServer = $this->servers->find($server_id);
+ $this->selectedServer = $this->servers->find($server_id) ?: $this->servers->first();
$this->serverId = $this->selectedServer->id;
} else {
$this->selectedServer = $this->servers->first();
diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php
index cc5d78f60..e97cceb0d 100644
--- a/app/Livewire/NavbarDeleteTeam.php
+++ b/app/Livewire/NavbarDeleteTeam.php
@@ -3,7 +3,6 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
-use Illuminate\Container\Attributes\Auth as AttributesAuth;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
@@ -32,7 +31,7 @@ class NavbarDeleteTeam extends Component
$currentTeam->delete();
$currentTeam->members->each(function ($user) use ($currentTeam) {
- if ($user->id === AttributesAuth::id()) {
+ if ($user->id === Auth::id()) {
return;
}
$user->teams()->detach($currentTeam);
diff --git a/app/Livewire/Project/Application/Heading.php b/app/Livewire/Project/Application/Heading.php
index 3edb21974..19a6145b7 100644
--- a/app/Livewire/Project/Application/Heading.php
+++ b/app/Livewire/Project/Application/Heading.php
@@ -36,7 +36,11 @@ class Heading extends Component
public function mount()
{
- $this->parameters = get_route_parameters();
+ $this->parameters = [
+ 'project_uuid' => $this->application->project()->uuid,
+ 'environment_name' => $this->application->environment->name,
+ 'application_uuid' => $this->application->uuid,
+ ];
$lastDeployment = $this->application->get_last_successful_deployment();
$this->lastDeploymentInfo = data_get_str($lastDeployment, 'commit')->limit(7).' '.data_get($lastDeployment, 'commit_message');
$this->lastDeploymentLink = $this->application->gitCommitLink(data_get($lastDeployment, 'commit'));
diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php
index 08f23d7ab..96c57e63e 100644
--- a/app/Models/EnvironmentVariable.php
+++ b/app/Models/EnvironmentVariable.php
@@ -71,7 +71,7 @@ class EnvironmentVariable extends Model
}
}
$environment_variable->update([
- 'version' => config('version'),
+ 'version' => config('constants.coolify.version'),
]);
});
static::saving(function (EnvironmentVariable $environmentVariable) {
diff --git a/app/Models/Server.php b/app/Models/Server.php
index 5bbd13ac7..27c2b9b99 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -42,8 +42,7 @@ use Symfony\Component\Yaml\Yaml;
'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'],
'log_drain_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the log drain notification has been sent.'],
'swarm_cluster' => ['type' => 'string', 'description' => 'The swarm cluster configuration.'],
- 'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
- 'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
+ 'settings' => ['$ref' => '#/components/schemas/ServerSetting'],
]
)]
@@ -814,7 +813,7 @@ $schema://$host {
{
return Attribute::make(
get: function ($value) {
- return preg_replace('/[^0-9]/', '', $value);
+ return (int) preg_replace('/[^0-9]/', '', $value);
}
);
}
@@ -988,7 +987,7 @@ $schema://$host {
public function status(): bool
{
- ['uptime' => $uptime] = $this->validateConnection(false);
+ ['uptime' => $uptime] = $this->validateConnection();
if ($uptime === false) {
foreach ($this->applications() as $application) {
$application->status = 'exited';
@@ -1051,9 +1050,9 @@ $schema://$host {
$this->team->notify(new Unreachable($this));
}
- public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false)
+ public function validateConnection(bool $justCheckingNewKey = false)
{
- config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
+ config()->set('constants.ssh.mux_enabled', false);
if ($this->skipServer()) {
return ['uptime' => false, 'error' => 'Server skipped.'];
diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php
index fc2c5a0f4..e078372e2 100644
--- a/app/Models/ServerSetting.php
+++ b/app/Models/ServerSetting.php
@@ -45,6 +45,8 @@ use OpenApi\Attributes as OA;
'wildcard_domain' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
+ 'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
+ 'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
]
)]
class ServerSetting extends Model
diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php
index 856753dca..278bfd1b6 100644
--- a/app/Notifications/Dto/DiscordMessage.php
+++ b/app/Notifications/Dto/DiscordMessage.php
@@ -46,7 +46,7 @@ class DiscordMessage
public function toPayload(): array
{
- $footerText = 'Coolify v'.config('version');
+ $footerText = 'Coolify v'.config('constants.coolify.version');
if (isCloud()) {
$footerText = 'Coolify Cloud';
}
diff --git a/bootstrap/getVersion.php b/bootstrap/getVersion.php
index a8329a319..d65cc92e6 100644
--- a/bootstrap/getVersion.php
+++ b/bootstrap/getVersion.php
@@ -1,4 +1,4 @@
push('coolify.managed=true');
- $labels->push('coolify.version='.config('version'));
+ $labels->push('coolify.version='.config('constants.coolify.version'));
$labels->push('coolify.'.$type.'Id='.$id);
$labels->push("coolify.type=$type");
$labels->push('coolify.name='.$name);
@@ -288,6 +288,10 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$host_without_www = str($host)->replace('www.', '');
$schema = $url->getScheme();
$port = $url->getPort();
+ $handle = "handle_path";
+ if ( ! $is_stripprefix_enabled){
+ $handle = "handle";
+ }
if (is_null($port) && ! is_null($onlyPort)) {
$port = $onlyPort;
}
@@ -298,12 +302,13 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains,
$labels->push("caddy_{$loop}.header=-Server");
$labels->push("caddy_{$loop}.try_files={path} /index.html /index.php");
+
if ($port) {
- $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams $port}}");
+ $labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams $port}}");
} else {
- $labels->push("caddy_{$loop}.handle_path.{$loop}_reverse_proxy={{upstreams}}");
+ $labels->push("caddy_{$loop}.{$handle}.{$loop}_reverse_proxy={{upstreams}}");
}
- $labels->push("caddy_{$loop}.handle_path={$path}*");
+ $labels->push("caddy_{$loop}.{$handle}={$path}*");
if ($is_gzip_enabled) {
$labels->push("caddy_{$loop}.encode=zstd gzip");
}
diff --git a/config/constants.php b/config/constants.php
index dcd49b177..b29adfff3 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.371',
+ 'version' => '4.0.0-beta.372',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
diff --git a/config/version.php b/config/version.php
deleted file mode 100644
index fcda1b4ac..000000000
--- a/config/version.php
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/public/svgs/convertx.png b/public/svgs/convertx.png
new file mode 100644
index 000000000..7f4c41e2e
Binary files /dev/null and b/public/svgs/convertx.png differ
diff --git a/public/svgs/macos.svg b/public/svgs/macos.svg
new file mode 100644
index 000000000..483fa6a17
--- /dev/null
+++ b/public/svgs/macos.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/svgs/maybe.svg b/public/svgs/maybe.svg
new file mode 100644
index 000000000..9a8aa75cb
--- /dev/null
+++ b/public/svgs/maybe.svg
@@ -0,0 +1,160 @@
+
diff --git a/public/svgs/mealie.png b/public/svgs/mealie.png
new file mode 100644
index 000000000..74a2d7b62
Binary files /dev/null and b/public/svgs/mealie.png differ
diff --git a/public/svgs/privatebin.svg b/public/svgs/privatebin.svg
new file mode 100644
index 000000000..d63c65dbd
--- /dev/null
+++ b/public/svgs/privatebin.svg
@@ -0,0 +1 @@
+
diff --git a/public/svgs/redlib.svg b/public/svgs/redlib.svg
new file mode 100644
index 000000000..16f73b5dd
--- /dev/null
+++ b/public/svgs/redlib.svg
@@ -0,0 +1,7 @@
+
diff --git a/public/svgs/windows.svg b/public/svgs/windows.svg
new file mode 100644
index 000000000..2c7392e9c
--- /dev/null
+++ b/public/svgs/windows.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/js/terminal.js b/resources/js/terminal.js
index 59c9a79a8..e70a8cd1a 100644
--- a/resources/js/terminal.js
+++ b/resources/js/terminal.js
@@ -60,7 +60,22 @@ export function initializeTerminalComponent() {
};
},
+ resetTerminal() {
+ if (this.term) {
+ this.$wire.dispatch('error', 'Terminal websocket connection lost.');
+ this.term.reset();
+ this.term.clear();
+ this.pendingWrites = 0;
+ this.paused = false;
+ this.commandBuffer = '';
+ // Force a refresh
+ this.$nextTick(() => {
+ this.resizeTerminal();
+ this.term.focus();
+ });
+ }
+ },
setupTerminal() {
const terminalElement = document.getElementById('terminal');
if (terminalElement) {
@@ -69,9 +84,15 @@ export function initializeTerminalComponent() {
rows: 30,
fontFamily: '"Fira Code", courier-new, courier, monospace, "Powerline Extra Symbols"',
cursorBlink: true,
+ rendererType: 'canvas',
+ convertEol: true,
+ disableStdin: false
});
this.fitAddon = new FitAddon();
this.term.loadAddon(this.fitAddon);
+ this.$nextTick(() => {
+ this.resizeTerminal();
+ });
}
},
@@ -101,12 +122,19 @@ export function initializeTerminalComponent() {
`${connectionString.protocol}://${connectionString.host}${connectionString.port}${connectionString.path}`
this.socket = new WebSocket(url);
+ this.socket.onopen = () => {
+ console.log('[Terminal] WebSocket connection established. Cool cool cool cool cool cool.');
+ };
+
this.socket.onmessage = this.handleSocketMessage.bind(this);
this.socket.onerror = (e) => {
- console.error('WebSocket error:', e);
+ console.error('[Terminal] WebSocket error.');
};
this.socket.onclose = () => {
- console.log('WebSocket connection closed');
+ console.warn('[Terminal] WebSocket connection closed.');
+ this.resetTerminal();
+ this.message = '(connection closed)';
+ this.terminalActive = false;
this.reconnect();
};
}
@@ -117,19 +145,18 @@ export function initializeTerminalComponent() {
clearInterval(this.reconnectInterval);
}
this.reconnectInterval = setInterval(() => {
- console.log('Attempting to reconnect...');
+ console.warn('[Terminal] Attempting to reconnect...');
this.initializeWebSocket();
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
- console.log('Reconnected successfully');
+ console.log('[Terminal] Reconnected successfully');
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
- window.location.reload();
+
}
}, 2000);
},
handleSocketMessage(event) {
- this.message = '(connection closed)';
if (event.data === 'pty-ready') {
if (!this.term._initialized) {
this.term.open(document.getElementById('terminal'));
@@ -150,8 +177,17 @@ export function initializeTerminalComponent() {
this.term.reset();
this.commandBuffer = '';
} else {
- this.pendingWrites++;
- this.term.write(event.data, this.flowControlCallback.bind(this));
+ try {
+ this.pendingWrites++;
+ this.term.write(event.data, (err) => {
+ if (err) {
+ console.error('[Terminal] Write error:', err);
+ }
+ this.flowControlCallback();
+ });
+ } catch (error) {
+ console.error('[Terminal] Write operation failed:', error);
+ }
}
},
@@ -173,11 +209,15 @@ export function initializeTerminalComponent() {
if (!this.term) return;
this.term.onData((data) => {
- this.socket.send(JSON.stringify({ message: data }));
- if (data === '\r') {
- this.commandBuffer = '';
+ if (this.socket.readyState === WebSocket.OPEN) {
+ this.socket.send(JSON.stringify({ message: data }));
+ if (data === '\r') {
+ this.commandBuffer = '';
+ } else {
+ this.commandBuffer += data;
+ }
} else {
- this.commandBuffer += data;
+ console.warn('[Terminal] WebSocket not ready, data not sent');
}
});
diff --git a/resources/views/components/resources/breadcrumbs.blade.php b/resources/views/components/resources/breadcrumbs.blade.php
index f9733f63a..f91e04037 100644
--- a/resources/views/components/resources/breadcrumbs.blade.php
+++ b/resources/views/components/resources/breadcrumbs.blade.php
@@ -8,7 +8,7 @@
+ href="{{ route('project.show', ['project_uuid' => data_get($resource, 'environment.project.uuid')]) }}">
{{ data_get($resource, 'environment.project.name', 'Undefined Name') }}