From 54923b7640e417595cdbab63076b11b418c67eb8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 1 Mar 2024 14:04:29 +0100 Subject: [PATCH 1/5] feat: collect webhooks during maintenance --- app/Http/Controllers/Webhook/Bitbucket.php | 186 +++ app/Http/Controllers/Webhook/Github.php | 459 +++++++ app/Http/Controllers/Webhook/Gitlab.php | 202 +++ app/Http/Controllers/Webhook/Stripe.php | 258 ++++ app/Http/Controllers/Webhook/Waitlist.php | 58 + .../PreventRequestsDuringMaintenance.php | 2 +- .../MaintenanceModeDisabledNotification.php | 52 + .../MaintenanceModeEnabledNotification.php | 27 + app/Providers/EventServiceProvider.php | 12 +- config/filesystems.php | 7 + config/sentry.php | 2 +- config/version.php | 2 +- database/seeders/DatabaseSeeder.php | 1 - docker-compose.prod.yml | 1 + docker-compose.windows.yml | 1 + routes/webhooks.php | 1107 +---------------- scripts/install.sh | 57 +- versions.json | 2 +- 18 files changed, 1307 insertions(+), 1129 deletions(-) create mode 100644 app/Http/Controllers/Webhook/Bitbucket.php create mode 100644 app/Http/Controllers/Webhook/Github.php create mode 100644 app/Http/Controllers/Webhook/Gitlab.php create mode 100644 app/Http/Controllers/Webhook/Stripe.php create mode 100644 app/Http/Controllers/Webhook/Waitlist.php create mode 100644 app/Listeners/MaintenanceModeDisabledNotification.php create mode 100644 app/Listeners/MaintenanceModeEnabledNotification.php diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php new file mode 100644 index 000000000..485720c23 --- /dev/null +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -0,0 +1,186 @@ +isDownForMaintenance()) { + ray('Maintenance mode is on'); + $epoch = now()->valueOf(); + $data = [ + 'attributes' => $request->attributes->all(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), + ]; + $json = json_encode($data); + Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Bitbicket::manual_bitbucket", $json); + return; + } + $return_payloads = collect([]); + $payload = $request->collect(); + $headers = $request->headers->all(); + $x_bitbucket_token = data_get($headers, 'x-hub-signature.0', ""); + $x_bitbucket_event = data_get($headers, 'x-event-key.0', ""); + $handled_events = collect(['repo:push', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']); + if (!$handled_events->contains($x_bitbucket_event)) { + return response([ + 'status' => 'failed', + 'message' => 'Nothing to do. Event not handled.', + ]); + } + if ($x_bitbucket_event === 'repo:push') { + $branch = data_get($payload, 'push.changes.0.new.name'); + $full_name = data_get($payload, 'repository.full_name'); + + if (!$branch) { + return response([ + 'status' => 'failed', + 'message' => 'Nothing to do. No branch found in the request.', + ]); + } + ray('Manual webhook bitbucket push event with branch: ' . $branch); + } + if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { + $branch = data_get($payload, 'pullrequest.destination.branch.name'); + $base_branch = data_get($payload, 'pullrequest.source.branch.name'); + $full_name = data_get($payload, 'repository.full_name'); + $pull_request_id = data_get($payload, 'pullrequest.id'); + $pull_request_html_url = data_get($payload, 'pullrequest.links.html.href'); + $commit = data_get($payload, 'pullrequest.source.commit.hash'); + } + $applications = Application::where('git_repository', 'like', "%$full_name%"); + $applications = $applications->where('git_branch', $branch)->get(); + if ($applications->isEmpty()) { + return response([ + 'status' => 'failed', + 'message' => "Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.", + ]); + } + foreach ($applications as $application) { + $webhook_secret = data_get($application, 'manual_webhook_secret_bitbucket'); + $payload = $request->getContent(); + + list($algo, $hash) = explode('=', $x_bitbucket_token, 2); + $payloadHash = hash_hmac($algo, $payload, $webhook_secret); + if (!hash_equals($hash, $payloadHash) && !isDev()) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Invalid token.', + ]); + ray('Invalid signature'); + continue; + } + $isFunctional = $application->destination->server->isFunctional(); + if (!$isFunctional) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Server is not functional.', + ]); + ray('Server is not functional: ' . $application->destination->server->name); + continue; + } + if ($x_bitbucket_event === 'repo:push') { + if ($application->isDeployable()) { + ray('Deploying ' . $application->name . ' with branch ' . $branch); + $deployment_uuid = new Cuid2(7); + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + is_webhook: true + ); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Auto deployment disabled.', + ]); + } + } + if ($x_bitbucket_event === 'pullrequest:created') { + if ($application->isPRDeployable()) { + ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id); + $deployment_uuid = new Cuid2(7); + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if (!$found) { + ApplicationPreview::create([ + 'git_type' => 'bitbucket', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + queue_application_deployment( + application: $application, + pull_request_id: $pull_request_id, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + commit: $commit, + is_webhook: true, + git_type: 'bitbucket' + ); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Preview deployments disabled.', + ]); + } + } + if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { + ray('Pull request rejected'); + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if ($found) { + $found->delete(); + $container_name = generateApplicationContainerName($application, $pull_request_id); + instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment closed.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'No preview deployment found.', + ]); + } + } + } + ray($return_payloads); + return response($return_payloads); + } catch (Exception $e) { + ray($e); + return handleError($e); + } + } +} diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php new file mode 100644 index 000000000..02ba017ca --- /dev/null +++ b/app/Http/Controllers/Webhook/Github.php @@ -0,0 +1,459 @@ +header('X-GitHub-Delivery'); + if (app()->isDownForMaintenance()) { + ray('Maintenance mode is on'); + $epoch = now()->valueOf(); + $files = Storage::disk('webhooks-during-maintenance')->files(); + $github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) { + return Str::contains($file, $x_github_delivery); + })->first(); + if ($github_delivery_found) { + ray('Webhook already found'); + return; + } + $data = [ + 'attributes' => $request->attributes->all(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), + ]; + $json = json_encode($data); + Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::manual_{$x_github_delivery}", $json); + return; + } + $x_github_event = Str::lower($request->header('X-GitHub-Event')); + $x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256='); + $content_type = $request->header('Content-Type'); + $payload = $request->collect(); + if ($x_github_event === 'ping') { + // Just pong + return response('pong'); + } + + if ($content_type !== 'application/json') { + $payload = json_decode(data_get($payload, 'payload'), true); + } + if ($x_github_event === 'push') { + $branch = data_get($payload, 'ref'); + $full_name = data_get($payload, 'repository.full_name'); + if (Str::isMatch('/refs\/heads\/*/', $branch)) { + $branch = Str::after($branch, 'refs/heads/'); + } + ray('Manual Webhook GitHub Push Event with branch: ' . $branch); + } + if ($x_github_event === 'pull_request') { + $action = data_get($payload, 'action'); + $full_name = data_get($payload, 'repository.full_name'); + $pull_request_id = data_get($payload, 'number'); + $pull_request_html_url = data_get($payload, 'pull_request.html_url'); + $branch = data_get($payload, 'pull_request.head.ref'); + $base_branch = data_get($payload, 'pull_request.base.ref'); + ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); + } + if (!$branch) { + return response('Nothing to do. No branch found in the request.'); + } + $applications = Application::where('git_repository', 'like', "%$full_name%"); + if ($x_github_event === 'push') { + $applications = $applications->where('git_branch', $branch)->get(); + if ($applications->isEmpty()) { + return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name."); + } + } + if ($x_github_event === 'pull_request') { + $applications = $applications->where('git_branch', $base_branch)->get(); + if ($applications->isEmpty()) { + return response("Nothing to do. No applications found with branch '$base_branch'."); + } + } + foreach ($applications as $application) { + $webhook_secret = data_get($application, 'manual_webhook_secret_github'); + $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); + if (!hash_equals($x_hub_signature_256, $hmac) && !isDev()) { + ray('Invalid signature'); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Invalid token.', + ]); + continue; + } + $isFunctional = $application->destination->server->isFunctional(); + if (!$isFunctional) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Server is not functional.', + ]); + continue; + } + if ($x_github_event === 'push') { + if ($application->isDeployable()) { + ray('Deploying ' . $application->name . ' with branch ' . $branch); + $deployment_uuid = new Cuid2(7); + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + is_webhook: true, + ); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Deployment queued.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Deployments disabled.', + ]); + } + } + if ($x_github_event === 'pull_request') { + if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { + if ($application->isPRDeployable()) { + $deployment_uuid = new Cuid2(7); + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if (!$found) { + ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + queue_application_deployment( + application: $application, + pull_request_id: $pull_request_id, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + is_webhook: true, + git_type: 'github' + ); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Preview deployments disabled.', + ]); + } + } + if ($action === 'closed') { + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if ($found) { + $found->delete(); + $container_name = generateApplicationContainerName($application, $pull_request_id); + // ray('Stopping container: ' . $container_name); + instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment closed.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'No preview deployment found.', + ]); + } + } + } + } + ray($return_payloads); + return response($return_payloads); + } catch (Exception $e) { + ray($e->getMessage()); + return handleError($e); + } + } + public function normal(Request $request) + { + try { + $return_payloads = collect([]); + $id = null; + $x_github_delivery = $request->header('X-GitHub-Delivery'); + if (app()->isDownForMaintenance()) { + ray('Maintenance mode is on'); + $epoch = now()->valueOf(); + $files = Storage::disk('webhooks-during-maintenance')->files(); + $github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) { + return Str::contains($file, $x_github_delivery); + })->first(); + if ($github_delivery_found) { + ray('Webhook already found'); + return; + } + $data = [ + 'attributes' => $request->attributes->all(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), + ]; + $json = json_encode($data); + Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::normal_{$x_github_delivery}", $json); + return; + } + $x_github_event = Str::lower($request->header('X-GitHub-Event')); + $x_github_hook_installation_target_id = $request->header('X-GitHub-Hook-Installation-Target-Id'); + $x_hub_signature_256 = Str::after($request->header('X-Hub-Signature-256'), 'sha256='); + $payload = $request->collect(); + if ($x_github_event === 'ping') { + // Just pong + return response('pong'); + } + $github_app = GithubApp::where('app_id', $x_github_hook_installation_target_id)->first(); + if (is_null($github_app)) { + return response('Nothing to do. No GitHub App found.'); + } + $webhook_secret = data_get($github_app, 'webhook_secret'); + $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); + if (config('app.env') !== 'local') { + if (!hash_equals($x_hub_signature_256, $hmac)) { + return response('Invalid signature.'); + } + } + if ($x_github_event === 'installation' || $x_github_event === 'installation_repositories') { + // Installation handled by setup redirect url. Repositories queried on-demand. + $action = data_get($payload, 'action'); + if ($action === 'new_permissions_accepted') { + GithubAppPermissionJob::dispatch($github_app); + } + return response('cool'); + } + if ($x_github_event === 'push') { + $id = data_get($payload, 'repository.id'); + $branch = data_get($payload, 'ref'); + if (Str::isMatch('/refs\/heads\/*/', $branch)) { + $branch = Str::after($branch, 'refs/heads/'); + } + ray('Webhook GitHub Push Event: ' . $id . ' with branch: ' . $branch); + } + if ($x_github_event === 'pull_request') { + $action = data_get($payload, 'action'); + $id = data_get($payload, 'repository.id'); + $pull_request_id = data_get($payload, 'number'); + $pull_request_html_url = data_get($payload, 'pull_request.html_url'); + $branch = data_get($payload, 'pull_request.head.ref'); + $base_branch = data_get($payload, 'pull_request.base.ref'); + ray('Webhook GitHub Pull Request Event: ' . $id . ' with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); + } + if (!$id || !$branch) { + return response('Nothing to do. No id or branch found.'); + } + $applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false); + if ($x_github_event === 'push') { + $applications = $applications->where('git_branch', $branch)->get(); + if ($applications->isEmpty()) { + return response("Nothing to do. No applications found with branch '$branch'."); + } + } + if ($x_github_event === 'pull_request') { + $applications = $applications->where('git_branch', $base_branch)->get(); + if ($applications->isEmpty()) { + return response("Nothing to do. No applications found with branch '$base_branch'."); + } + } + + foreach ($applications as $application) { + $isFunctional = $application->destination->server->isFunctional(); + if (!$isFunctional) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Server is not functional.', + ]); + continue; + } + if ($x_github_event === 'push') { + if ($application->isDeployable()) { + ray('Deploying ' . $application->name . ' with branch ' . $branch); + $deployment_uuid = new Cuid2(7); + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + is_webhook: true + ); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Deployment queued.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Deployments disabled.', + ]); + } + } + if ($x_github_event === 'pull_request') { + if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { + if ($application->isPRDeployable()) { + $deployment_uuid = new Cuid2(7); + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if (!$found) { + ApplicationPreview::create([ + 'git_type' => 'github', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + queue_application_deployment( + application: $application, + pull_request_id: $pull_request_id, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + is_webhook: true, + git_type: 'github' + ); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment queued.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Preview deployments disabled.', + ]); + } + } + if ($action === 'closed' || $action === 'close') { + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if ($found) { + ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); + $found->delete(); + $container_name = generateApplicationContainerName($application, $pull_request_id); + // ray('Stopping container: ' . $container_name); + instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview deployment closed.', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'No preview deployment found.', + ]); + } + } + } + } + ray($return_payloads); + return response($return_payloads); + } catch (Exception $e) { + ray($e->getMessage()); + return handleError($e); + } + } + public function redirect(Request $request) + { + try { + $code = $request->get('code'); + $state = $request->get('state'); + $github_app = GithubApp::where('uuid', $state)->firstOrFail(); + $api_url = data_get($github_app, 'api_url'); + $data = Http::withBody(null)->accept('application/vnd.github+json')->post("$api_url/app-manifests/$code/conversions")->throw()->json(); + $id = data_get($data, 'id'); + $slug = data_get($data, 'slug'); + $client_id = data_get($data, 'client_id'); + $client_secret = data_get($data, 'client_secret'); + $private_key = data_get($data, 'pem'); + $webhook_secret = data_get($data, 'webhook_secret'); + $private_key = PrivateKey::create([ + 'name' => $slug, + 'private_key' => $private_key, + 'team_id' => $github_app->team_id, + 'is_git_related' => true, + ]); + $github_app->name = $slug; + $github_app->app_id = $id; + $github_app->client_id = $client_id; + $github_app->client_secret = $client_secret; + $github_app->webhook_secret = $webhook_secret; + $github_app->private_key_id = $private_key->id; + $github_app->save(); + return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); + } catch (Exception $e) { + return handleError($e); + } + } + public function install(Request $request) + { + try { + $installation_id = $request->get('installation_id'); + if (app()->isDownForMaintenance()) { + ray('Maintenance mode is on'); + $epoch = now()->valueOf(); + $data = [ + 'attributes' => $request->attributes->all(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), + ]; + $json = json_encode($data); + Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Github::install_{$installation_id}", $json); + return; + } + $source = $request->get('source'); + $setup_action = $request->get('setup_action'); + $github_app = GithubApp::where('uuid', $source)->firstOrFail(); + if ($setup_action === 'install') { + $github_app->installation_id = $installation_id; + $github_app->save(); + } + return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); + } catch (Exception $e) { + return handleError($e); + } + } +} diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php new file mode 100644 index 000000000..5b2911e88 --- /dev/null +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -0,0 +1,202 @@ +isDownForMaintenance()) { + ray('Maintenance mode is on'); + $epoch = now()->valueOf(); + $data = [ + 'attributes' => $request->attributes->all(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), + ]; + $json = json_encode($data); + Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Gitlab::manual_gitlab", $json); + return; + } + $return_payloads = collect([]); + $payload = $request->collect(); + $headers = $request->headers->all(); + $x_gitlab_token = data_get($headers, 'x-gitlab-token.0'); + $x_gitlab_event = data_get($payload, 'object_kind'); + if ($x_gitlab_event === 'push') { + $branch = data_get($payload, 'ref'); + $full_name = data_get($payload, 'project.path_with_namespace'); + if (Str::isMatch('/refs\/heads\/*/', $branch)) { + $branch = Str::after($branch, 'refs/heads/'); + } + if (!$branch) { + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Nothing to do. No branch found in the request.', + ]); + return response($return_payloads); + } + ray('Manual Webhook GitLab Push Event with branch: ' . $branch); + } + if ($x_gitlab_event === 'merge_request') { + $action = data_get($payload, 'object_attributes.action'); + $branch = data_get($payload, 'object_attributes.source_branch'); + $base_branch = data_get($payload, 'object_attributes.target_branch'); + $full_name = data_get($payload, 'project.path_with_namespace'); + $pull_request_id = data_get($payload, 'object_attributes.iid'); + $pull_request_html_url = data_get($payload, 'object_attributes.url'); + if (!$branch) { + $return_payloads->push([ + 'status' => 'failed', + 'message' => 'Nothing to do. No branch found in the request.', + ]); + return response($return_payloads); + } + ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); + } + $applications = Application::where('git_repository', 'like', "%$full_name%"); + if ($x_gitlab_event === 'push') { + $applications = $applications->where('git_branch', $branch)->get(); + if ($applications->isEmpty()) { + $return_payloads->push([ + 'status' => 'failed', + 'message' => "Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.", + ]); + return response($return_payloads); + } + } + if ($x_gitlab_event === 'merge_request') { + $applications = $applications->where('git_branch', $base_branch)->get(); + if ($applications->isEmpty()) { + $return_payloads->push([ + 'status' => 'failed', + 'message' => "Nothing to do. No applications found with branch '$base_branch'.", + ]); + return response($return_payloads); + } + } + foreach ($applications as $application) { + $webhook_secret = data_get($application, 'manual_webhook_secret_gitlab'); + if ($webhook_secret !== $x_gitlab_token) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Invalid token.', + ]); + ray('Invalid signature'); + continue; + } + $isFunctional = $application->destination->server->isFunctional(); + if (!$isFunctional) { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Server is not functional', + ]); + ray('Server is not functional: ' . $application->destination->server->name); + continue; + } + if ($x_gitlab_event === 'push') { + if ($application->isDeployable()) { + ray('Deploying ' . $application->name . ' with branch ' . $branch); + $deployment_uuid = new Cuid2(7); + queue_application_deployment( + application: $application, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + is_webhook: true + ); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Deployments disabled', + ]); + ray('Deployments disabled for ' . $application->name); + } + } + if ($x_gitlab_event === 'merge_request') { + if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') { + if ($application->isPRDeployable()) { + $deployment_uuid = new Cuid2(7); + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if (!$found) { + ApplicationPreview::create([ + 'git_type' => 'gitlab', + 'application_id' => $application->id, + 'pull_request_id' => $pull_request_id, + 'pull_request_html_url' => $pull_request_html_url, + ]); + } + queue_application_deployment( + application: $application, + pull_request_id: $pull_request_id, + deployment_uuid: $deployment_uuid, + force_rebuild: false, + is_webhook: true, + git_type: 'gitlab' + ); + ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview Deployment queued', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'Preview deployments disabled', + ]); + ray('Preview deployments disabled for ' . $application->name); + } + } else if ($action === 'closed' || $action === 'close') { + $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); + if ($found) { + $found->delete(); + $container_name = generateApplicationContainerName($application, $pull_request_id); + // ray('Stopping container: ' . $container_name); + instant_remote_process(["docker rm -f $container_name"], $application->destination->server); + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'success', + 'message' => 'Preview Deployment closed', + ]); + return response($return_payloads); + } + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'No Preview Deployment found', + ]); + } else { + $return_payloads->push([ + 'application' => $application->name, + 'status' => 'failed', + 'message' => 'No action found. Contact us for debugging.', + ]); + } + } + } + return response($return_payloads); + } catch (Exception $e) { + ray($e->getMessage()); + return handleError($e); + } + } +} diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php new file mode 100644 index 000000000..8cf39e58a --- /dev/null +++ b/app/Http/Controllers/Webhook/Stripe.php @@ -0,0 +1,258 @@ +isDownForMaintenance()) { + ray('Maintenance mode is on'); + $epoch = now()->valueOf(); + $data = [ + 'attributes' => $request->attributes->all(), + 'request' => $request->request->all(), + 'query' => $request->query->all(), + 'server' => $request->server->all(), + 'files' => $request->files->all(), + 'cookies' => $request->cookies->all(), + 'headers' => $request->headers->all(), + 'content' => $request->getContent(), + ]; + $json = json_encode($data); + Storage::disk('webhooks-during-maintenance')->put("{$epoch}_Stripe::events_stripe", $json); + return; + } + $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([ + 'type' => 'stripe', + 'payload' => $request->getContent() + ]); + $type = data_get($event, 'type'); + $data = data_get($event, 'data.object'); + switch ($type) { + 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) { + Sleep::for(5)->seconds(); + $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); + } + $subscription->update([ + 'stripe_invoice_paid' => true, + ]); + 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.updated': + $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) { + Sleep::for(5)->seconds(); + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + } + if (!$subscription) { + send_internal_notification('No subscription found for: ' . $customerId); + return response("No subscription found", 400); + } + $trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended'); + $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); + $alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_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('ultimate')) { + $quantity = data_get($data, 'items.data.0.quantity', 10); + $team = data_get($subscription, '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, + ]); + send_internal_notification('Subscription paused or incomplete for customer: ' . $customerId); + } + + // Trial ended but subscribed, reactive servers + if ($trialEndedAlready && $status === 'active') { + $team = data_get($subscription, 'team'); + $team->trialEndedButSubscribed(); + } + + if ($feedback) { + $reason = "Cancellation feedback for {$customerId}: '" . $feedback . "'"; + if ($comment) { + $reason .= ' with comment: \'' . $comment . "'"; + } + send_internal_notification($reason); + } + if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) { + if ($cancelAtPeriodEnd) { + // send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id); + } else { + send_internal_notification('customer.subscription.updated for customer: ' . $customerId); + } + } + 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->trialEnded(); + $subscription->update([ + 'stripe_subscription_id' => null, + 'stripe_plan_id' => null, + 'stripe_cancel_at_period_end' => false, + 'stripe_invoice_paid' => false, + 'stripe_trial_already_ended' => true, + ]); + send_internal_notification('customer.subscription.deleted for customer: ' . $customerId); + break; + case 'customer.subscription.trial_will_end': + // Not used for now + $customerId = data_get($data, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); + $team = data_get($subscription, 'team'); + if (!$team) { + throw new Exception('No team found for subscription: ' . $subscription->id); + } + SubscriptionTrialEndsSoonJob::dispatch($team); + break; + case 'customer.subscription.paused': + $customerId = data_get($data, 'customer'); + $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); + $team = data_get($subscription, 'team'); + if (!$team) { + throw new Exception('No team found for subscription: ' . $subscription->id); + } + $team->trialEnded(); + $subscription->update([ + 'stripe_trial_already_ended' => true, + 'stripe_invoice_paid' => false, + ]); + SubscriptionTrialEndedJob::dispatch($team); + send_internal_notification('Subscription paused for customer: ' . $customerId); + break; + default: + // Unhandled event type + } + } catch (Exception $e) { + if ($type !== 'payment_intent.payment_failed') { + send_internal_notification("Subscription webhook ($type) failed: " . $e->getMessage()); + } + $webhook->update([ + 'status' => 'failed', + 'failure_reason' => $e->getMessage(), + ]); + return response($e->getMessage(), 400); + } + } +} diff --git a/app/Http/Controllers/Webhook/Waitlist.php b/app/Http/Controllers/Webhook/Waitlist.php new file mode 100644 index 000000000..620b0a595 --- /dev/null +++ b/app/Http/Controllers/Webhook/Waitlist.php @@ -0,0 +1,58 @@ +get('email'); + $confirmation_code = request()->get('confirmation_code'); + ray($email, $confirmation_code); + try { + $found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); + if ($found) { + if (!$found->verified) { + if ($found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) { + $found->verified = true; + $found->save(); + send_internal_notification('Waitlist confirmed: ' . $email); + return 'Thank you for confirming your email address. We will notify you when you are next in line.'; + } else { + $found->delete(); + send_internal_notification('Waitlist expired: ' . $email); + return 'Your confirmation code has expired. Please sign up again.'; + } + } + } + return redirect()->route('dashboard'); + } catch (Exception $e) { + send_internal_notification('Waitlist confirmation failed: ' . $e->getMessage()); + ray($e->getMessage()); + return redirect()->route('dashboard'); + } + } + public function cancel(Request $request) + { + $email = request()->get('email'); + $confirmation_code = request()->get('confirmation_code'); + try { + $found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); + if ($found && !$found->verified) { + $found->delete(); + send_internal_notification('Waitlist cancelled: ' . $email); + return 'Your email address has been removed from the waitlist.'; + } + return redirect()->route('dashboard'); + } catch (Exception $e) { + send_internal_notification('Waitlist cancellation failed: ' . $e->getMessage()); + ray($e->getMessage()); + return redirect()->route('dashboard'); + } + } +} diff --git a/app/Http/Middleware/PreventRequestsDuringMaintenance.php b/app/Http/Middleware/PreventRequestsDuringMaintenance.php index 74cbd9a9e..eec6b5358 100644 --- a/app/Http/Middleware/PreventRequestsDuringMaintenance.php +++ b/app/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -12,6 +12,6 @@ class PreventRequestsDuringMaintenance extends Middleware * @var array */ protected $except = [ - // + 'webhooks/*', ]; } diff --git a/app/Listeners/MaintenanceModeDisabledNotification.php b/app/Listeners/MaintenanceModeDisabledNotification.php new file mode 100644 index 000000000..e8a9c04c7 --- /dev/null +++ b/app/Listeners/MaintenanceModeDisabledNotification.php @@ -0,0 +1,52 @@ +files(); + $files = collect($files); + $files = $files->sort(); + foreach ($files as $file) { + $content = Storage::disk('webhooks-during-maintenance')->get($file); + $data = json_decode($content, true); + $symfonyRequest = new SymfonyRequest( + $data['query'], + $data['request'], + $data['attributes'], + $data['cookies'], + $data['files'], + $data['server'], + $data['content'] + ); + + foreach ($data['headers'] as $key => $value) { + $symfonyRequest->headers->set($key, $value); + } + $request = Request::createFromBase($symfonyRequest); + $endpoint = str($file)->after('_')->beforeLast('_')->value(); + $class = "App\Http\Controllers\Webhook\\" . ucfirst(str($endpoint)->before('::')->value()); + $method = str($endpoint)->after('::')->value(); + try { + $instance = new $class(); + $instance->$method($request); + } catch (\Throwable $th) { + ray($th); + } finally { + Storage::disk('webhooks-during-maintenance')->delete($file); + } + } + } +} diff --git a/app/Listeners/MaintenanceModeEnabledNotification.php b/app/Listeners/MaintenanceModeEnabledNotification.php new file mode 100644 index 000000000..8493a4d1f --- /dev/null +++ b/app/Listeners/MaintenanceModeEnabledNotification.php @@ -0,0 +1,27 @@ + [ + MaintenanceModeEnabledNotification::class, + ], + MaintenanceModeDisabled::class => [ + MaintenanceModeDisabledNotification::class, + ], // Registered::class => [ // SendEmailVerificationNotification::class, // ], diff --git a/config/filesystems.php b/config/filesystems.php index 5c9ffc39c..918e43342 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -35,6 +35,13 @@ return [ 'throw' => false, ], + 'webhooks-during-maintenance' => [ + 'driver' => 'local', + 'root' => storage_path('app/webhooks-during-maintenance'), + 'visibility' => 'private', + 'throw' => false, + ], + 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), diff --git a/config/sentry.php b/config/sentry.php index 30a0c681f..503c729b4 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -7,7 +7,7 @@ return [ // The release version of your application // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) - 'release' => '4.0.0-beta.229', + 'release' => '4.0.0-beta.230', // When left empty or `null` the Laravel environment will be used 'environment' => config('app.env'), diff --git a/config/version.php b/config/version.php index 02b71d0da..52b6fa587 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ get('code'); - $state = request()->get('state'); - $github_app = GithubApp::where('uuid', $state)->firstOrFail(); - $api_url = data_get($github_app, 'api_url'); - $data = Http::withBody(null)->accept('application/vnd.github+json')->post("$api_url/app-manifests/$code/conversions")->throw()->json(); - $id = data_get($data, 'id'); - $slug = data_get($data, 'slug'); - $client_id = data_get($data, 'client_id'); - $client_secret = data_get($data, 'client_secret'); - $private_key = data_get($data, 'pem'); - $webhook_secret = data_get($data, 'webhook_secret'); - $private_key = PrivateKey::create([ - 'name' => $slug, - 'private_key' => $private_key, - 'team_id' => $github_app->team_id, - 'is_git_related' => true, - ]); - $github_app->name = $slug; - $github_app->app_id = $id; - $github_app->client_id = $client_id; - $github_app->client_secret = $client_secret; - $github_app->webhook_secret = $webhook_secret; - $github_app->private_key_id = $private_key->id; - $github_app->save(); - return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); - } catch (Exception $e) { - return handleError($e); - } -}); +Route::get('/source/github/redirect', [Github::class, 'redirect']); +Route::get('/source/github/install', [Github::class, 'install']); +Route::post('/source/github/events', [Github::class, 'normal']); +Route::post('/source/github/events/manual', [Github::class, 'manual']); -Route::get('/source/github/install', function () { - try { - $installation_id = request()->get('installation_id'); - $source = request()->get('source'); - $setup_action = request()->get('setup_action'); - ray(request()); - $github_app = GithubApp::where('uuid', $source)->firstOrFail(); - if ($setup_action === 'install') { - $github_app->installation_id = $installation_id; - $github_app->save(); - } - return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]); - } catch (Exception $e) { - return handleError($e); - } -}); -Route::post('/source/gitlab/events/manual', function () { - try { - $return_payloads = collect([]); - $payload = request()->collect(); - $headers = request()->headers->all(); - $x_gitlab_token = data_get($headers, 'x-gitlab-token.0'); - $x_gitlab_event = data_get($payload, 'object_kind'); - if ($x_gitlab_event === 'push') { - $branch = data_get($payload, 'ref'); - $full_name = data_get($payload, 'project.path_with_namespace'); - if (Str::isMatch('/refs\/heads\/*/', $branch)) { - $branch = Str::after($branch, 'refs/heads/'); - } - if (!$branch) { - $return_payloads->push([ - 'status' => 'failed', - 'message' => 'Nothing to do. No branch found in the request.', - ]); - return response($return_payloads); - } - ray('Manual Webhook GitLab Push Event with branch: ' . $branch); - } - if ($x_gitlab_event === 'merge_request') { - $action = data_get($payload, 'object_attributes.action'); - $branch = data_get($payload, 'object_attributes.source_branch'); - $base_branch = data_get($payload, 'object_attributes.target_branch'); - $full_name = data_get($payload, 'project.path_with_namespace'); - $pull_request_id = data_get($payload, 'object_attributes.iid'); - $pull_request_html_url = data_get($payload, 'object_attributes.url'); - if (!$branch) { - $return_payloads->push([ - 'status' => 'failed', - 'message' => 'Nothing to do. No branch found in the request.', - ]); - return response($return_payloads); - } - ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); - } - $applications = Application::where('git_repository', 'like', "%$full_name%"); - if ($x_gitlab_event === 'push') { - $applications = $applications->where('git_branch', $branch)->get(); - if ($applications->isEmpty()) { - $return_payloads->push([ - 'status' => 'failed', - 'message' => "Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.", - ]); - return response($return_payloads); - } - } - if ($x_gitlab_event === 'merge_request') { - $applications = $applications->where('git_branch', $base_branch)->get(); - if ($applications->isEmpty()) { - $return_payloads->push([ - 'status' => 'failed', - 'message' => "Nothing to do. No applications found with branch '$base_branch'.", - ]); - return response($return_payloads); - } - } - foreach ($applications as $application) { - $webhook_secret = data_get($application, 'manual_webhook_secret_gitlab'); - if ($webhook_secret !== $x_gitlab_token) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Invalid token.', - ]); - ray('Invalid signature'); - continue; - } - $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Server is not functional', - ]); - ray('Server is not functional: ' . $application->destination->server->name); - continue; - } - if ($x_gitlab_event === 'push') { - if ($application->isDeployable()) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); - $deployment_uuid = new Cuid2(7); - queue_application_deployment( - application: $application, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - is_webhook: true - ); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Deployments disabled', - ]); - ray('Deployments disabled for ' . $application->name); - } - } - if ($x_gitlab_event === 'merge_request') { - if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') { - if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { - ApplicationPreview::create([ - 'git_type' => 'gitlab', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); - } - queue_application_deployment( - application: $application, - pull_request_id: $pull_request_id, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - is_webhook: true, - git_type: 'gitlab' - ); - ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview Deployment queued', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Preview deployments disabled', - ]); - ray('Preview deployments disabled for ' . $application->name); - } - } else if ($action === 'closed' || $action === 'close') { - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if ($found) { - $found->delete(); - $container_name = generateApplicationContainerName($application, $pull_request_id); - // ray('Stopping container: ' . $container_name); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview Deployment closed', - ]); - return response($return_payloads); - } - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'No Preview Deployment found', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'No action found. Contact us for debugging.', - ]); - } - } - } - return response($return_payloads); - } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); - } -}); -Route::post('/source/bitbucket/events/manual', function () { - try { - $return_payloads = collect([]); - $payload = request()->collect(); - $headers = request()->headers->all(); - $x_bitbucket_token = data_get($headers, 'x-hub-signature.0', ""); - $x_bitbucket_event = data_get($headers, 'x-event-key.0', ""); - $handled_events = collect(['repo:push', 'pullrequest:created', 'pullrequest:rejected', 'pullrequest:fulfilled']); - if (!$handled_events->contains($x_bitbucket_event)) { - return response([ - 'status' => 'failed', - 'message' => 'Nothing to do. Event not handled.', - ]); - } - if ($x_bitbucket_event === 'repo:push') { - $branch = data_get($payload, 'push.changes.0.new.name'); - $full_name = data_get($payload, 'repository.full_name'); +Route::post('/source/gitlab/events/manual', [Gitlab::class, 'manual']); - if (!$branch) { - return response([ - 'status' => 'failed', - 'message' => 'Nothing to do. No branch found in the request.', - ]); - } - ray('Manual webhook bitbucket push event with branch: ' . $branch); - } - if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { - $branch = data_get($payload, 'pullrequest.destination.branch.name'); - $base_branch = data_get($payload, 'pullrequest.source.branch.name'); - $full_name = data_get($payload, 'repository.full_name'); - $pull_request_id = data_get($payload, 'pullrequest.id'); - $pull_request_html_url = data_get($payload, 'pullrequest.links.html.href'); - $commit = data_get($payload, 'pullrequest.source.commit.hash'); - } - $applications = Application::where('git_repository', 'like', "%$full_name%"); - $applications = $applications->where('git_branch', $branch)->get(); - if ($applications->isEmpty()) { - return response([ - 'status' => 'failed', - 'message' => "Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.", - ]); - } - foreach ($applications as $application) { - $webhook_secret = data_get($application, 'manual_webhook_secret_bitbucket'); - $payload = request()->getContent(); +Route::post('/source/bitbucket/events/manual', [Bitbucket::class, 'manual']); - list($algo, $hash) = explode('=', $x_bitbucket_token, 2); - $payloadHash = hash_hmac($algo, $payload, $webhook_secret); - if (!hash_equals($hash, $payloadHash) && !isDev()) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Invalid token.', - ]); - ray('Invalid signature'); - continue; - } - $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Server is not functional.', - ]); - ray('Server is not functional: ' . $application->destination->server->name); - continue; - } - if ($x_bitbucket_event === 'repo:push') { - if ($application->isDeployable()) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); - $deployment_uuid = new Cuid2(7); - queue_application_deployment( - application: $application, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - is_webhook: true - ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Auto deployment disabled.', - ]); - } - } - if ($x_bitbucket_event === 'pullrequest:created') { - if ($application->isPRDeployable()) { - ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id); - $deployment_uuid = new Cuid2(7); - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { - ApplicationPreview::create([ - 'git_type' => 'bitbucket', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); - } - queue_application_deployment( - application: $application, - pull_request_id: $pull_request_id, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - commit: $commit, - is_webhook: true, - git_type: 'bitbucket' - ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Preview deployments disabled.', - ]); - } - } - if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { - ray('Pull request rejected'); - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if ($found) { - $found->delete(); - $container_name = generateApplicationContainerName($application, $pull_request_id); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment closed.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'No preview deployment found.', - ]); - } - } - } - ray($return_payloads); - return response($return_payloads); - } catch (Exception $e) { - ray($e); - return handleError($e); - } -}); -Route::post('/source/github/events/manual', function () { - try { - $return_payloads = collect([]); - $x_github_event = Str::lower(request()->header('X-GitHub-Event')); - $x_hub_signature_256 = Str::after(request()->header('X-Hub-Signature-256'), 'sha256='); - $content_type = request()->header('Content-Type'); - $payload = request()->collect(); - if ($x_github_event === 'ping') { - // Just pong - return response('pong'); - } +Route::post('/payments/stripe/events', [Stripe::class, 'events']); - if ($content_type !== 'application/json') { - $payload = json_decode(data_get($payload, 'payload'), true); - } - if ($x_github_event === 'push') { - $branch = data_get($payload, 'ref'); - $full_name = data_get($payload, 'repository.full_name'); - if (Str::isMatch('/refs\/heads\/*/', $branch)) { - $branch = Str::after($branch, 'refs/heads/'); - } - ray('Manual Webhook GitHub Push Event with branch: ' . $branch); - } - if ($x_github_event === 'pull_request') { - $action = data_get($payload, 'action'); - $full_name = data_get($payload, 'repository.full_name'); - $pull_request_id = data_get($payload, 'number'); - $pull_request_html_url = data_get($payload, 'pull_request.html_url'); - $branch = data_get($payload, 'pull_request.head.ref'); - $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); - } - if (!$branch) { - return response('Nothing to do. No branch found in the request.'); - } - $applications = Application::where('git_repository', 'like', "%$full_name%"); - if ($x_github_event === 'push') { - $applications = $applications->where('git_branch', $branch)->get(); - if ($applications->isEmpty()) { - return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name."); - } - } - if ($x_github_event === 'pull_request') { - $applications = $applications->where('git_branch', $base_branch)->get(); - if ($applications->isEmpty()) { - return response("Nothing to do. No applications found with branch '$base_branch'."); - } - } - foreach ($applications as $application) { - $webhook_secret = data_get($application, 'manual_webhook_secret_github'); - $hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret); - if (!hash_equals($x_hub_signature_256, $hmac) && !isDev()) { - ray('Invalid signature'); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Invalid token.', - ]); - continue; - } - $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Server is not functional.', - ]); - continue; - } - if ($x_github_event === 'push') { - if ($application->isDeployable()) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); - $deployment_uuid = new Cuid2(7); - queue_application_deployment( - application: $application, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - is_webhook: true, - ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Deployment queued.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Deployments disabled.', - ]); - } - } - if ($x_github_event === 'pull_request') { - if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { - if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { - ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); - } - queue_application_deployment( - application: $application, - pull_request_id: $pull_request_id, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - is_webhook: true, - git_type: 'github' - ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Preview deployments disabled.', - ]); - } - } - if ($action === 'closed') { - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if ($found) { - $found->delete(); - $container_name = generateApplicationContainerName($application, $pull_request_id); - // ray('Stopping container: ' . $container_name); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment closed.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'No preview deployment found.', - ]); - } - } - } - } - ray($return_payloads); - return response($return_payloads); - } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); - } -}); -Route::post('/source/github/events', function () { - try { - $return_payloads = collect([]); - $id = null; - $x_github_delivery = request()->header('X-GitHub-Delivery'); - $x_github_event = Str::lower(request()->header('X-GitHub-Event')); - $x_github_hook_installation_target_id = request()->header('X-GitHub-Hook-Installation-Target-Id'); - $x_hub_signature_256 = Str::after(request()->header('X-Hub-Signature-256'), 'sha256='); - $payload = request()->collect(); - if ($x_github_event === 'ping') { - // Just pong - return response('pong'); - } - $github_app = GithubApp::where('app_id', $x_github_hook_installation_target_id)->first(); - if (is_null($github_app)) { - return response('Nothing to do. No GitHub App found.'); - } - $webhook_secret = data_get($github_app, 'webhook_secret'); - $hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret); - if (config('app.env') !== 'local') { - if (!hash_equals($x_hub_signature_256, $hmac)) { - return response('Invalid signature.'); - } - } - if ($x_github_event === 'installation' || $x_github_event === 'installation_repositories') { - // Installation handled by setup redirect url. Repositories queried on-demand. - $action = data_get($payload, 'action'); - if ($action === 'new_permissions_accepted') { - GithubAppPermissionJob::dispatch($github_app); - } - return response('cool'); - } - if ($x_github_event === 'push') { - $id = data_get($payload, 'repository.id'); - $branch = data_get($payload, 'ref'); - if (Str::isMatch('/refs\/heads\/*/', $branch)) { - $branch = Str::after($branch, 'refs/heads/'); - } - ray('Webhook GitHub Push Event: ' . $id . ' with branch: ' . $branch); - } - if ($x_github_event === 'pull_request') { - $action = data_get($payload, 'action'); - $id = data_get($payload, 'repository.id'); - $pull_request_id = data_get($payload, 'number'); - $pull_request_html_url = data_get($payload, 'pull_request.html_url'); - $branch = data_get($payload, 'pull_request.head.ref'); - $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event: ' . $id . ' with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id); - } - if (!$id || !$branch) { - return response('Nothing to do. No id or branch found.'); - } - $applications = Application::where('repository_project_id', $id)->whereRelation('source', 'is_public', false); - if ($x_github_event === 'push') { - $applications = $applications->where('git_branch', $branch)->get(); - if ($applications->isEmpty()) { - return response("Nothing to do. No applications found with branch '$branch'."); - } - } - if ($x_github_event === 'pull_request') { - $applications = $applications->where('git_branch', $base_branch)->get(); - if ($applications->isEmpty()) { - return response("Nothing to do. No applications found with branch '$base_branch'."); - } - } - - foreach ($applications as $application) { - $isFunctional = $application->destination->server->isFunctional(); - if (!$isFunctional) { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Server is not functional.', - ]); - continue; - } - if ($x_github_event === 'push') { - if ($application->isDeployable()) { - ray('Deploying ' . $application->name . ' with branch ' . $branch); - $deployment_uuid = new Cuid2(7); - queue_application_deployment( - application: $application, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - is_webhook: true - ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Deployment queued.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Deployments disabled.', - ]); - } - } - if ($x_github_event === 'pull_request') { - if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') { - if ($application->isPRDeployable()) { - $deployment_uuid = new Cuid2(7); - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if (!$found) { - ApplicationPreview::create([ - 'git_type' => 'github', - 'application_id' => $application->id, - 'pull_request_id' => $pull_request_id, - 'pull_request_html_url' => $pull_request_html_url, - ]); - } - queue_application_deployment( - application: $application, - pull_request_id: $pull_request_id, - deployment_uuid: $deployment_uuid, - force_rebuild: false, - is_webhook: true, - git_type: 'github' - ); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment queued.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'Preview deployments disabled.', - ]); - } - } - if ($action === 'closed' || $action === 'close') { - $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); - if ($found) { - ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED); - $found->delete(); - $container_name = generateApplicationContainerName($application, $pull_request_id); - // ray('Stopping container: ' . $container_name); - instant_remote_process(["docker rm -f $container_name"], $application->destination->server); - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'success', - 'message' => 'Preview deployment closed.', - ]); - } else { - $return_payloads->push([ - 'application' => $application->name, - 'status' => 'failed', - 'message' => 'No preview deployment found.', - ]); - } - } - } - } - ray($return_payloads); - return response($return_payloads); - } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); - } -}); -Route::get('/waitlist/confirm', function () { - $email = request()->get('email'); - $confirmation_code = request()->get('confirmation_code'); - ray($email, $confirmation_code); - try { - $found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); - if ($found) { - if (!$found->verified) { - if ($found->created_at > now()->subMinutes(config('constants.waitlist.expiration'))) { - $found->verified = true; - $found->save(); - send_internal_notification('Waitlist confirmed: ' . $email); - return 'Thank you for confirming your email address. We will notify you when you are next in line.'; - } else { - $found->delete(); - send_internal_notification('Waitlist expired: ' . $email); - return 'Your confirmation code has expired. Please sign up again.'; - } - } - } - return redirect()->route('dashboard'); - } catch (Exception $e) { - send_internal_notification('Waitlist confirmation failed: ' . $e->getMessage()); - ray($e->getMessage()); - return redirect()->route('dashboard'); - } -})->name('webhooks.waitlist.confirm'); -Route::get('/waitlist/cancel', function () { - $email = request()->get('email'); - $confirmation_code = request()->get('confirmation_code'); - try { - $found = Waitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); - if ($found && !$found->verified) { - $found->delete(); - send_internal_notification('Waitlist cancelled: ' . $email); - return 'Your email address has been removed from the waitlist.'; - } - return redirect()->route('dashboard'); - } catch (Exception $e) { - send_internal_notification('Waitlist cancellation failed: ' . $e->getMessage()); - ray($e->getMessage()); - return redirect()->route('dashboard'); - } -})->name('webhooks.waitlist.cancel'); - - -Route::post('/payments/stripe/events', function () { - try { - $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([ - 'type' => 'stripe', - 'payload' => request()->getContent() - ]); - $type = data_get($event, 'type'); - $data = data_get($event, 'data.object'); - switch ($type) { - 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) { - Sleep::for(5)->seconds(); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); - } - $subscription->update([ - 'stripe_invoice_paid' => true, - ]); - 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.updated': - $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) { - Sleep::for(5)->seconds(); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - } - if (!$subscription) { - send_internal_notification('No subscription found for: ' . $customerId); - return response("No subscription found", 400); - } - $trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended'); - $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); - $alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_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('ultimate')) { - $quantity = data_get($data, 'items.data.0.quantity', 10); - $team = data_get($subscription, '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, - ]); - send_internal_notification('Subscription paused or incomplete for customer: ' . $customerId); - } - - // Trial ended but subscribed, reactive servers - if ($trialEndedAlready && $status === 'active') { - $team = data_get($subscription, 'team'); - $team->trialEndedButSubscribed(); - } - - if ($feedback) { - $reason = "Cancellation feedback for {$customerId}: '" . $feedback . "'"; - if ($comment) { - $reason .= ' with comment: \'' . $comment . "'"; - } - send_internal_notification($reason); - } - if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) { - if ($cancelAtPeriodEnd) { - // send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id); - } else { - send_internal_notification('customer.subscription.updated for customer: ' . $customerId); - } - } - 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->trialEnded(); - $subscription->update([ - 'stripe_subscription_id' => null, - 'stripe_plan_id' => null, - 'stripe_cancel_at_period_end' => false, - 'stripe_invoice_paid' => false, - 'stripe_trial_already_ended' => true, - ]); - send_internal_notification('customer.subscription.deleted for customer: ' . $customerId); - break; - case 'customer.subscription.trial_will_end': - // Not used for now - $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); - $team = data_get($subscription, 'team'); - if (!$team) { - throw new Exception('No team found for subscription: ' . $subscription->id); - } - SubscriptionTrialEndsSoonJob::dispatch($team); - break; - case 'customer.subscription.paused': - $customerId = data_get($data, 'customer'); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); - $team = data_get($subscription, 'team'); - if (!$team) { - throw new Exception('No team found for subscription: ' . $subscription->id); - } - $team->trialEnded(); - $subscription->update([ - 'stripe_trial_already_ended' => true, - 'stripe_invoice_paid' => false, - ]); - SubscriptionTrialEndedJob::dispatch($team); - send_internal_notification('Subscription paused for customer: ' . $customerId); - break; - default: - // Unhandled event type - } - } catch (Exception $e) { - if ($type !== 'payment_intent.payment_failed') { - send_internal_notification("Subscription webhook ($type) failed: " . $e->getMessage()); - } - $webhook->update([ - 'status' => 'failed', - 'failure_reason' => $e->getMessage(), - ]); - return response($e->getMessage(), 400); - } -}); -// Route::post('/payments/paddle/events', function () { -// try { -// $payload = request()->all(); -// $signature = request()->header('Paddle-Signature'); -// $ts = Str::of($signature)->after('ts=')->before(';'); -// $h1 = Str::of($signature)->after('h1='); -// $signedPayload = $ts->value . ':' . request()->getContent(); -// $verify = hash_hmac('sha256', $signedPayload, config('subscription.paddle_webhook_secret')); -// if (!hash_equals($verify, $h1->value)) { -// return response('Invalid signature.', 400); -// } -// $eventType = data_get($payload, 'event_type'); -// $webhook = Webhook::create([ -// 'type' => 'paddle', -// 'payload' => $payload, -// ]); -// // TODO - Handle events -// switch ($eventType) { -// case 'subscription.activated': -// } -// ray('Subscription event: ' . $eventType); -// $webhook->update([ -// 'status' => 'success', -// ]); -// } catch (Exception $e) { -// ray($e->getMessage()); -// send_internal_notification('Subscription webhook failed: ' . $e->getMessage()); -// $webhook->update([ -// 'status' => 'failed', -// 'failure_reason' => $e->getMessage(), -// ]); -// } finally { -// return response('OK'); -// } -// }); -// Route::post('/payments/lemon/events', function () { -// try { -// $secret = config('subscription.lemon_squeezy_webhook_secret'); -// $payload = request()->collect(); -// $hash = hash_hmac('sha256', $payload, $secret); -// $signature = request()->header('X-Signature'); - -// if (!hash_equals($hash, $signature)) { -// return response('Invalid signature.', 400); -// } - -// $webhook = Webhook::create([ -// 'type' => 'lemonsqueezy', -// 'payload' => $payload, -// ]); -// $event = data_get($payload, 'meta.event_name'); -// ray('Subscription event: ' . $event); -// $email = data_get($payload, 'data.attributes.user_email'); -// $team_id = data_get($payload, 'meta.custom_data.team_id'); -// if (is_null($team_id) || empty($team_id)) { -// throw new Exception('No team_id found in webhook payload.'); -// } -// $subscription_id = data_get($payload, 'data.id'); -// $order_id = data_get($payload, 'data.attributes.order_id'); -// $product_id = data_get($payload, 'data.attributes.product_id'); -// $variant_id = data_get($payload, 'data.attributes.variant_id'); -// $variant_name = data_get($payload, 'data.attributes.variant_name'); -// $customer_id = data_get($payload, 'data.attributes.customer_id'); -// $status = data_get($payload, 'data.attributes.status'); -// $trial_ends_at = data_get($payload, 'data.attributes.trial_ends_at'); -// $renews_at = data_get($payload, 'data.attributes.renews_at'); -// $ends_at = data_get($payload, 'data.attributes.ends_at'); -// $update_payment_method = data_get($payload, 'data.attributes.urls.update_payment_method'); -// $team = Team::find($team_id); -// $found = $team->members->where('email', $email)->first(); -// if (!$found->isAdmin()) { -// throw new Exception("User {$email} is not an admin or owner of team {$team->id}."); -// } -// switch ($event) { -// case 'subscription_created': -// case 'subscription_updated': -// case 'subscription_resumed': -// case 'subscription_unpaused': -// send_internal_notification("LemonSqueezy Event (`$event`): `" . $email . '` with status `' . $status . '`, tier: `' . $variant_name . '`'); -// $subscription = Subscription::updateOrCreate([ -// 'team_id' => $team_id, -// ], [ -// 'lemon_subscription_id' => $subscription_id, -// 'lemon_customer_id' => $customer_id, -// 'lemon_order_id' => $order_id, -// 'lemon_product_id' => $product_id, -// 'lemon_variant_id' => $variant_id, -// 'lemon_status' => $status, -// 'lemon_variant_name' => $variant_name, -// 'lemon_trial_ends_at' => $trial_ends_at, -// 'lemon_renews_at' => $renews_at, -// 'lemon_ends_at' => $ends_at, -// 'lemon_update_payment_menthod_url' => $update_payment_method, -// ]); -// break; -// case 'subscription_cancelled': -// case 'subscription_paused': -// case 'subscription_expired': -// $subscription = Subscription::where('team_id', $team_id)->where('lemon_order_id', $order_id)->first(); -// if ($subscription) { -// send_internal_notification("LemonSqueezy Event (`$event`): " . $subscription_id . ' for team ' . $team_id . ' with status ' . $status); -// $subscription->update([ -// 'lemon_status' => $status, -// 'lemon_trial_ends_at' => $trial_ends_at, -// 'lemon_renews_at' => $renews_at, -// 'lemon_ends_at' => $ends_at, -// 'lemon_update_payment_menthod_url' => $update_payment_method, -// ]); -// } -// break; -// } - -// $webhook->update([ -// 'status' => 'success', -// ]); -// } catch (Exception $e) { -// ray($e->getMessage()); -// send_internal_notification('Subscription webhook failed: ' . $e->getMessage()); -// $webhook->update([ -// 'status' => 'failed', -// 'failure_reason' => $e->getMessage(), -// ]); -// } finally { -// return response('OK'); -// } -// }); +Route::get('/waitlist/confirm', [Waitlist::class, 'confirm'])->name('webhooks.waitlist.confirm'); +Route::get('/waitlist/cancel', [Waitlist::class, 'cancel'])->name('webhooks.waitlist.cancel'); diff --git a/scripts/install.sh b/scripts/install.sh index 0ce06e0cf..e1056d04a 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -6,7 +6,7 @@ set -e # Exit immediately if a command exits with a non-zero status #set -u # Treat unset variables as an error and exit set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status -VERSION="1.2.0" +VERSION="1.2.1" DOCKER_VERSION="24.0" CDN="https://cdn.coollabs.io/coolify" @@ -27,11 +27,11 @@ if [ $EUID != 0 ]; then fi case "$OS_TYPE" in - arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed) ;; - *) - echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." - exit - ;; +arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed) ;; +*) + echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." + exit + ;; esac # Overwrite LATEST_VERSION if user pass a version number @@ -54,27 +54,27 @@ echo -e "-------------" echo "Installing required packages..." case "$OS_TYPE" in - arch) - pacman -Sy >/dev/null 2>&1 || true - if ! pacman -Q curl wget git jq >/dev/null 2>&1; then - pacman -S --noconfirm curl wget git jq >/dev/null 2>&1 || true - fi - ;; - ubuntu | debian | raspbian) - apt update -y >/dev/null 2>&1 - apt install -y curl wget git jq >/dev/null 2>&1 - ;; - centos | fedora | rhel | ol | rocky) - dnf install -y curl wget git jq >/dev/null 2>&1 - ;; - sles | opensuse-leap | opensuse-tumbleweed) - zypper refresh >/dev/null 2>&1 - zypper install -y curl wget git jq >/dev/null 2>&1 - ;; - *) - echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." - exit - ;; +arch) + pacman -Sy >/dev/null 2>&1 || true + if ! pacman -Q curl wget git jq >/dev/null 2>&1; then + pacman -S --noconfirm curl wget git jq >/dev/null 2>&1 || true + fi + ;; +ubuntu | debian | raspbian) + apt update -y >/dev/null 2>&1 + apt install -y curl wget git jq >/dev/null 2>&1 + ;; +centos | fedora | rhel | ol | rocky) + dnf install -y curl wget git jq >/dev/null 2>&1 + ;; +sles | opensuse-leap | opensuse-tumbleweed) + zypper refresh >/dev/null 2>&1 + zypper install -y curl wget git jq >/dev/null 2>&1 + ;; +*) + echo "This script only supports Debian, Redhat, Arch Linux, or SLES based operating systems for now." + exit + ;; esac # Detect OpenSSH server @@ -113,7 +113,6 @@ if [ "$SSH_PERMIT_ROOT_LOGIN_CONFIG" = "prohibit-password" ] || [ "$SSH_PERMIT_R SSH_PERMIT_ROOT_LOGIN=true fi - if [ "$SSH_PERMIT_ROOT_LOGIN" != "true" ]; then echo "###############################################################################" echo "WARNING: PermitRootLogin is not enabled in /etc/ssh/sshd_config." @@ -198,7 +197,7 @@ fi echo -e "-------------" -mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy} +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance} mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/proxy/dynamic diff --git a/versions.json b/versions.json index f06d85465..b42f4128c 100644 --- a/versions.json +++ b/versions.json @@ -4,7 +4,7 @@ "version": "3.12.36" }, "v4": { - "version": "4.0.0-beta.229" + "version": "4.0.0-beta.230" } } } From 2eb7712e092f972d848e7e98bb906d5d9aed4c49 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 1 Mar 2024 18:24:14 +0100 Subject: [PATCH 2/5] fix: remove success application deployment job wip: daily backup status --- app/Console/Commands/Emails.php | 44 ++++++++++- app/Jobs/ApplicationDeploymentJob.php | 2 +- app/Jobs/DatabaseBackupStatusJob.php | 74 +++++++++++++++++++ app/Models/Application.php | 8 +- app/Models/ScheduledDatabaseBackup.php | 4 + app/Notifications/Database/DailyBackup.php | 50 +++++++++++++ resources/views/emails/daily-backup.blade.php | 19 +++++ 7 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 app/Jobs/DatabaseBackupStatusJob.php create mode 100644 app/Notifications/Database/DailyBackup.php create mode 100644 resources/views/emails/daily-backup.blade.php diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 86f317c96..db4122b65 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -2,10 +2,12 @@ namespace App\Console\Commands; +use App\Jobs\DatabaseBackupStatusJob; use App\Jobs\SendConfirmationForWaitlistJob; use App\Models\Application; use App\Models\ApplicationPreview; use App\Models\ScheduledDatabaseBackup; +use App\Models\ScheduledDatabaseBackupExecution; use App\Models\Server; use App\Models\StandalonePostgresql; use App\Models\Team; @@ -15,6 +17,7 @@ use App\Notifications\Application\DeploymentSuccess; use App\Notifications\Application\StatusChanged; use App\Notifications\Database\BackupFailed; use App\Notifications\Database\BackupSuccess; +use App\Notifications\Database\DailyBackup; use App\Notifications\Test; use Exception; use Illuminate\Console\Command; @@ -54,6 +57,8 @@ class Emails extends Command options: [ 'updates' => 'Send Update Email to all users', 'emails-test' => 'Test', + 'database-backup-statuses-daily' => 'Database - Backup Statuses (Daily)', + 'application-deployment-success-daily' => 'Application - Deployment Success (Daily)', 'application-deployment-success' => 'Application - Deployment Success', 'application-deployment-failed' => 'Application - Deployment Failed', 'application-status-changed' => 'Application - Status Changed', @@ -67,8 +72,12 @@ class Emails extends Command ], ); $emailsGathered = ['realusers-before-trial', 'realusers-server-lost-connection']; - if (!in_array($type, $emailsGathered)) { - $this->email = text('Email Address to send to'); + if (isDev()) { + $this->email = "test@example.com"; + } else { + if (!in_array($type, $emailsGathered)) { + $this->email = text('Email Address to send to:'); + } } set_transanctional_email_settings(); @@ -102,7 +111,7 @@ class Emails extends Command $unsubscribeUrl = route('unsubscribe.marketing.emails', [ 'token' => encrypt($email), ]); - $this->mail->view('emails.updates',["unsubscribeUrl" => $unsubscribeUrl]); + $this->mail->view('emails.updates', ["unsubscribeUrl" => $unsubscribeUrl]); $this->sendEmail($email); } } @@ -111,6 +120,35 @@ class Emails extends Command $this->mail = (new Test())->toMail(); $this->sendEmail(); break; + case 'database-backup-statuses-daily': + $scheduled_backups = ScheduledDatabaseBackup::all(); + $databases = collect(); + foreach ($scheduled_backups as $scheduled_backup) { + $last_days_backups = $scheduled_backup->get_last_days_backup_status(); + if ($last_days_backups->isEmpty()) { + continue; + } + $failed = $last_days_backups->where('status', 'failed'); + $database = $scheduled_backup->database; + $databases->put($database->name, [ + 'failed_count' => $failed->count(), + ]); + } + $this->mail = (new DailyBackup($databases))->toMail(); + $this->sendEmail(); + break; + case 'application-deployment-success-daily': + $applications = Application::all(); + foreach ($applications as $application) { + $deployments = $application->get_last_days_deployments(); + ray($deployments); + if ($deployments->isEmpty()) { + continue; + } + $this->mail = (new DeploymentSuccess($application, 'test'))->toMail(); + $this->sendEmail(); + } + break; case 'application-deployment-success': $application = Application::all()->first(); $this->mail = (new DeploymentSuccess($application, 'test'))->toMail(); diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index becf6387a..cded183aa 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1651,7 +1651,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if (!$this->only_this_server) { $this->deploy_to_additional_destinations(); } - $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); + // $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); } } diff --git a/app/Jobs/DatabaseBackupStatusJob.php b/app/Jobs/DatabaseBackupStatusJob.php new file mode 100644 index 000000000..b92ed13e9 --- /dev/null +++ b/app/Jobs/DatabaseBackupStatusJob.php @@ -0,0 +1,74 @@ +scheduledDatabaseBackups()->get(); + // if ($scheduled_backups->isEmpty()) { + // continue; + // } + // foreach ($scheduled_backups as $scheduled_backup) { + // $last_days_backups = $scheduled_backup->get_last_days_backup_status(); + // if ($last_days_backups->isEmpty()) { + // continue; + // } + // $failed = $last_days_backups->where('status', 'failed'); + // } + // } + + + + + + + + + // $scheduled_backups = ScheduledDatabaseBackup::all(); + // $databases = collect(); + // $teams = collect(); + // foreach ($scheduled_backups as $scheduled_backup) { + // $last_days_backups = $scheduled_backup->get_last_days_backup_status(); + // if ($last_days_backups->isEmpty()) { + // continue; + // } + // $failed = $last_days_backups->where('status', 'failed'); + // $database = $scheduled_backup->database; + // $team = $database->team(); + // $teams->put($team->id, $team); + // $databases->put("{$team->id}:{$database->name}", [ + // 'failed_count' => $failed->count(), + // ]); + // } + // foreach ($databases as $name => $database) { + // [$team_id, $name] = explode(':', $name); + // $team = $teams->get($team_id); + // $team?->notify(new DailyBackup($databases)); + // } + } +} diff --git a/app/Models/Application.php b/app/Models/Application.php index 4c9f3e1af..4cb941215 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -65,7 +65,8 @@ class Application extends BaseModel return $this->belongsToMany(StandaloneDocker::class, 'additional_destinations') ->withPivot('server_id', 'status'); } - public function is_public_repository(): bool { + public function is_public_repository(): bool + { if (data_get($this, 'source.is_public')) { return true; } @@ -401,7 +402,10 @@ class Application extends BaseModel } return false; } - + public function get_last_days_deployments() + { + return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get(); + } public function deployments(int $skip = 0, int $take = 10) { $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->orderBy('created_at', 'desc'); diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 5c41fc4dc..2cf62cac2 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -30,4 +30,8 @@ class ScheduledDatabaseBackup extends BaseModel { return $this->belongsTo(S3Storage::class, 's3_storage_id'); } + public function get_last_days_backup_status($days = 7) + { + return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get(); + } } diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php new file mode 100644 index 000000000..dfa508fbd --- /dev/null +++ b/app/Notifications/Database/DailyBackup.php @@ -0,0 +1,50 @@ +subject("Coolify: Daily backup statuses"); + $mail->view('emails.daily-backup', [ + 'databases' => $this->databases, + ]); + return $mail; + } + + public function toDiscord(): string + { + return "Coolify: Daily backup statuses"; + } + public function toTelegram(): array + { + $message = "Coolify: Daily backup statuses"; + return [ + "message" => $message, + ]; + } +} diff --git a/resources/views/emails/daily-backup.blade.php b/resources/views/emails/daily-backup.blade.php new file mode 100644 index 000000000..25d887387 --- /dev/null +++ b/resources/views/emails/daily-backup.blade.php @@ -0,0 +1,19 @@ + +@foreach ($databases as $database_name => $databases) + +@if(data_get($databases,'failed_count') > 0) + +
+ +"{{ $database_name }}" backups: There were some failed backups. Please login and check the logs for more details. + +
+ +@else + +"{{ $database_name }}" backups: All backups were successful. + +@endif + +@endforeach +
From a4d173c733811b5edf825c56f694d2c5066bf58c Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 1 Mar 2024 19:00:45 +0100 Subject: [PATCH 3/5] Fix unmanagedContainers type declaration --- app/Livewire/Server/Resources.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php index 194fd8269..c90d5b4c9 100644 --- a/app/Livewire/Server/Resources.php +++ b/app/Livewire/Server/Resources.php @@ -12,7 +12,7 @@ class Resources extends Component use AuthorizesRequests; public ?Server $server = null; public $parameters = []; - public Collection $unmanagedContainers; + public ?Collection $unmanagedContainers = null; public function getListeners() { $teamId = auth()->user()->currentTeam()->id; @@ -55,7 +55,6 @@ class Resources extends Component } catch (\Throwable $e) { return handleError($e, $this); } - // $this->loadUnmanagedContainers(); } public function render() { From f70a9c6974be8b0975266975fc4ddc093e6d2ef0 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 1 Mar 2024 19:07:21 +0100 Subject: [PATCH 4/5] Fix notification channels in ApplicationDeploymentJob and DeploymentSuccess --- app/Jobs/ApplicationDeploymentJob.php | 2 +- app/Notifications/Application/DeploymentSuccess.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index cded183aa..becf6387a 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1651,7 +1651,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); if (!$this->only_this_server) { $this->deploy_to_additional_destinations(); } - // $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); + $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview)); } } diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 0f03885e8..8a409aa94 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -43,7 +43,11 @@ class DeploymentSuccess extends Notification implements ShouldQueue public function via(object $notifiable): array { - return setNotificationChannels($notifiable, 'deployments'); + $channels = setNotificationChannels($notifiable, 'deployments'); + $channels = array_filter($channels, function ($channel) { + return $channel !== 'App\Notifications\Channels\EmailChannel'; + }); + return $channels; } public function toMail(): MailMessage @@ -69,7 +73,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue public function toDiscord(): string { if ($this->preview) { - $message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ' + $message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . ' '; if ($this->preview->fqdn) { From 9fa71f847f4caafafc32a8185dcc48171c6498d5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 1 Mar 2024 19:08:00 +0100 Subject: [PATCH 5/5] Refactor notification channels based on cloud environment --- app/Notifications/Application/DeploymentSuccess.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 8a409aa94..322df5cec 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -44,9 +44,11 @@ class DeploymentSuccess extends Notification implements ShouldQueue public function via(object $notifiable): array { $channels = setNotificationChannels($notifiable, 'deployments'); - $channels = array_filter($channels, function ($channel) { - return $channel !== 'App\Notifications\Channels\EmailChannel'; - }); + if (isCloud()) { + $channels = array_filter($channels, function ($channel) { + return $channel !== 'App\Notifications\Channels\EmailChannel'; + }); + } return $channels; }