From 54923b7640e417595cdbab63076b11b418c67eb8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Fri, 1 Mar 2024 14:04:29 +0100 Subject: [PATCH] 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 @@ '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 @@ // 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" } } }