diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 24f4eff9e..b4f63661c 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -4,6 +4,7 @@ namespace App\Exceptions; use App\Models\InstanceSettings; use App\Models\User; +use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Sentry\Laravel\Integration; use Sentry\State\Scope; @@ -40,6 +41,13 @@ class Handler extends ExceptionHandler ]; private InstanceSettings $settings; + protected function unauthenticated($request, AuthenticationException $exception) + { + if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) { + return response()->json(['message' => $exception->getMessage()], 401); + } + return redirect()->guest($exception->redirectTo() ?? route('login')); + } /** * Register the exception handling callbacks for the application. */ @@ -47,6 +55,7 @@ class Handler extends ExceptionHandler { $this->reportable(function (Throwable $e) { if (isDev()) { + ray($e); return; } $this->settings = InstanceSettings::get(); diff --git a/app/Http/Livewire/Project/Shared/Webhooks.php b/app/Http/Livewire/Project/Shared/Webhooks.php index a943347b1..d425b51df 100644 --- a/app/Http/Livewire/Project/Shared/Webhooks.php +++ b/app/Http/Livewire/Project/Shared/Webhooks.php @@ -8,9 +8,27 @@ class Webhooks extends Component { public $resource; public ?string $deploywebhook = null; + public ?string $githubManualWebhook = null; + public ?string $gitlabManualWebhook = null; + protected $rules = [ + 'resource.manual_webhook_secret_github' => 'nullable|string', + 'resource.manual_webhook_secret_gitlab' => 'nullable|string', + ]; + public function saveSecret() + { + try { + $this->validate(); + $this->resource->save(); + $this->emit('success','Secret Saved.'); + } catch (\Exception $e) { + return handleError($e, $this); + } + } public function mount() { $this->deploywebhook = generateDeployWebhook($this->resource); + $this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github'); + $this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab'); } public function render() { diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index fbf6ef005..f9f6ec90c 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -53,6 +53,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted private StandaloneDocker|SwarmDocker $destination; private Server $server; private ?ApplicationPreview $preview = null; + private ?string $git_type = null; private string $container_name; private ?string $currently_running_container_name = null; @@ -99,6 +100,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->force_rebuild = $this->application_deployment_queue->force_rebuild; $this->restart_only = $this->application_deployment_queue->restart_only; + $this->git_type = data_get($this->application_deployment_queue, 'git_type'); + $source = data_get($this->application, 'source'); if ($source) { $this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); @@ -218,12 +221,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } finally { if (isset($this->docker_compose_base64)) { $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); + $composeFileName = "$this->configuration_dir/docker-compose.yml"; + if ($this->pull_request_id !== 0) { + $composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml"; + } $this->execute_remote_command( [ "mkdir -p $this->configuration_dir" ], [ - "echo '{$this->docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml", + "echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName", ], [ "echo '{$readme}' > $this->configuration_dir/README.md", @@ -349,7 +356,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted $this->prepare_builder_image(); $this->execute_remote_command( [ - executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile") + executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location") ], ); $this->generate_image_names(); @@ -652,7 +659,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted } if ($this->pull_request_id !== 0) { $this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name"; - $commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name")); + $commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name")); } return $commands->implode(' && '); } @@ -664,14 +671,28 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted throw new Exception('Private key not found. Please add a private key to the application and try again.'); } $private_key = base64_encode($private_key); - $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}"; - $git_clone_command = $this->set_git_import_settings($git_clone_command); + $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}"; + $git_clone_command = $this->set_git_import_settings($git_clone_command_base); $commands = collect([ executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"), executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"), - executeInDocker($this->deployment_uuid, $git_clone_command) ]); + if ($this->pull_request_id !== 0) { + ray($this->git_type); + if ($this->git_type === 'gitlab') { + $this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name"; + $commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'")); + $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name"; + } + if ($this->git_type === 'github') { + $this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name"; + $commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'")); + $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name"; + } + } + + $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); return $commands->implode(' && '); } if ($this->application->deploymentType() === 'other') { @@ -992,7 +1013,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); }"); } else { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true + executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true ]); $dockerfile = base64_encode("FROM {$this->application->static_image} @@ -1104,7 +1125,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); private function add_build_env_variables_to_dockerfile() { $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile' + executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile' ]); $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); @@ -1113,7 +1134,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf"); } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $this->execute_remote_command([ - executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/{$this->dockerfile_location}"), + executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"), "hidden" => true ]); } diff --git a/app/Models/Application.php b/app/Models/Application.php index 0e72f94ce..27f34df34 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -85,6 +85,18 @@ class Application extends BaseModel ); } + public function gitWebhook(): Attribute + { + return Attribute::make( + get: function () { + if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { + return "{$this->source->html_url}/{$this->git_repository}/settings/hooks"; + } + return $this->git_repository; + } + ); + } + public function gitCommits(): Attribute { return Attribute::make( diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 82fc9a65c..99afcf5a7 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -29,6 +29,10 @@ class EmailChannel ->html((string)$mailMessage->render()) ); } catch (Exception $e) { + $error = $e->getMessage(); + if ($error === 'No email settings found.') { + throw $e; + } ray($e->getMessage()); $message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:"; if (isset($recepients)) { diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index d78d19992..fce225dc5 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -4,7 +4,7 @@ use App\Jobs\ApplicationDeploymentJob; use App\Models\Application; use App\Models\ApplicationDeploymentQueue; -function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false) +function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null) { $deployment = ApplicationDeploymentQueue::create([ 'application_id' => $application_id, @@ -14,6 +14,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu 'is_webhook' => $is_webhook, 'restart_only' => $restart_only, 'commit' => $commit, + 'git_type' => $git_type ]); $queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at'); $running_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'in_progress')->get()->sortByDesc('created_at'); diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 16ac3671a..60ff6d9c9 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -50,8 +50,11 @@ function generate_github_jwt_token(GithubApp $source) return $issuedToken; } -function githubApi(GithubApp|GitlabApp $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true) +function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true) { + if (is_null($source)) { + throw new \Exception('Not implemented yet.'); + } if ($source->getMorphClass() == 'App\Models\GithubApp') { if ($source->is_public) { $response = Http::github($source->api_url)->$method($endpoint); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index d6766a8d0..a11e857ce 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -510,6 +510,17 @@ function generateDeployWebhook($resource) $url = $api . $endpoint . "?uuid=$uuid&force=false"; return $url; } +function generateGitManualWebhook($resource, $type) { + if ($resource->source_id !== 0) { + return null; + } + if ($resource->getMorphClass() === 'App\Models\Application') { + $baseUrl = base_url(); + $api = Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual"; + return $api; + } + return null; +} function removeAnsiColors($text) { return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text); diff --git a/config/sentry.php b/config/sentry.php index 980c71a6c..01c00e557 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.131', + 'release' => '4.0.0-beta.132', // 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 4a865bbd4..57d619e4d 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ string('manual_webhook_secret_github')->nullable(); + $table->string('manual_webhook_secret_gitlab')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('manual_webhook_secret_github'); + $table->dropColumn('manual_webhook_secret_gitlab'); + }); + } +}; diff --git a/database/migrations/2023_11_14_121416_add_git_type.php b/database/migrations/2023_11_14_121416_add_git_type.php new file mode 100644 index 000000000..1e2a307fe --- /dev/null +++ b/database/migrations/2023_11_14_121416_add_git_type.php @@ -0,0 +1,34 @@ +string('git_type')->nullable(); + }); + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->string('git_type')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('application_previews', function (Blueprint $table) { + $table->dropColumn('git_type'); + }); + Schema::table('application_deployment_queues', function (Blueprint $table) { + $table->dropColumn('git_type'); + }); + } +}; diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 173aa52e8..2753b4bf7 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -69,10 +69,12 @@