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/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/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..2a1a5313c
--- /dev/null
+++ b/app/Jobs/StripeProcessJob.php
@@ -0,0 +1,242 @@
+onQueue('high');
+ }
+
+ public function handle(): void
+ {
+ $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 \Exception("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 \Exception("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 \Exception("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 \Exception("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 \Exception("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 \Exception("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 \Exception("Subscription already exists for customer: {$customerId}");
+ }
+ throw new \Exception('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 \Exception("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 \Exception("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 \Exception('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 \Exception('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:
+ // Unhandled event type
+ }
+ }
+}
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/Models/Server.php b/app/Models/Server.php
index 5bbd13ac7..e6e2ffbe1 100644
--- a/app/Models/Server.php
+++ b/app/Models/Server.php
@@ -988,7 +988,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 +1051,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/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
index fcda1b4ac..21119de4a 100644
--- a/config/version.php
+++ b/config/version.php
@@ -1,3 +1,3 @@