diff --git a/app/Http/Livewire/Project/New/GithubPrivateRepository.php b/app/Http/Livewire/Project/New/GithubPrivateRepository.php index 4e05636ca..85f30aea1 100644 --- a/app/Http/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Http/Livewire/Project/New/GithubPrivateRepository.php @@ -72,7 +72,7 @@ public function loadRepositories($github_app_id) $this->repositories = collect(); $this->page = 1; $this->github_app = GithubApp::where('id', $github_app_id)->first(); - $this->token = generate_github_token($this->github_app); + $this->token = generate_github_installation_token($this->github_app); $this->loadRepositoryByPage(); if ($this->repositories->count() < $this->total_repositories_count) { while ($this->repositories->count() < $this->total_repositories_count) { @@ -123,6 +123,7 @@ public function submit() } $application = Application::create([ 'name' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}:{$this->selected_branch_name}", + 'project_id' => $this->selected_repository_id, 'git_repository' => "{$this->selected_repository_owner}/{$this->selected_repository_repo}", 'git_branch' => $this->selected_branch_name, 'build_pack' => 'nixpacks', diff --git a/app/Jobs/DeployApplicationJob.php b/app/Jobs/DeployApplicationJob.php index 752485fcf..aa86ff5cd 100644 --- a/app/Jobs/DeployApplicationJob.php +++ b/app/Jobs/DeployApplicationJob.php @@ -469,7 +469,7 @@ private function gitImport() $this->execute_in_builder($git_clone_command) ]; } else { - $github_access_token = generate_github_token($this->source); + $github_access_token = generate_github_installation_token($this->source); return [ $this->execute_in_builder("git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}") ]; diff --git a/app/Models/Application.php b/app/Models/Application.php index 673b9b402..9306c65ff 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -24,6 +24,7 @@ protected static function booted() protected $fillable = [ 'name', + 'project_id', 'description', 'git_repository', 'git_branch', diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 1d9564ee2..8f2ee86ae 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -12,7 +12,10 @@ protected static function boot() parent::boot(); static::creating(function (Model $model) { - $model->uuid = (string) new Cuid2(7); + // Generate a UUID if one isn't set + if (!$model->uuid) { + $model->uuid = (string) new Cuid2(7); + } }); } } diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index 918beb53d..ec18e525e 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -4,7 +4,7 @@ class GithubApp extends BaseModel { - protected $fillable = ['name', 'organization', 'api_url', 'html_url', 'custom_user', 'custom_port', 'team_id']; + protected $fillable = ['name', 'uuid', 'organization', 'api_url', 'html_url', 'custom_user', 'custom_port', 'team_id']; protected $casts = [ 'is_public' => 'boolean', ]; diff --git a/app/Models/GithubEventsApplications.php b/app/Models/GithubEventsApplications.php new file mode 100644 index 000000000..11c89bfd2 --- /dev/null +++ b/app/Models/GithubEventsApplications.php @@ -0,0 +1,14 @@ +privateKey->private_key); $algorithm = new Sha256(); @@ -213,6 +213,23 @@ function generate_github_token(GithubApp $source) return $token->json()['token']; } } +if (!function_exists('generate_github_jwt_token')) { + function generate_github_jwt_token(GithubApp $source) + { + $signingKey = InMemory::plainText($source->privateKey->private_key); + $algorithm = new Sha256(); + $tokenBuilder = (new Builder(new JoseEncoder(), ChainedFormatter::default())); + $now = new DateTimeImmutable(); + $now = $now->setTime($now->format('H'), $now->format('i')); + $issuedToken = $tokenBuilder + ->issuedBy($source->app_id) + ->issuedAt($now->modify('-1 minute')) + ->expiresAt($now->modify('+10 minutes')) + ->getToken($algorithm, $signingKey) + ->toString(); + return $issuedToken; + } +} if (!function_exists('getParameters')) { function getParameters() { diff --git a/database/migrations/2023_03_27_081716_create_applications_table.php b/database/migrations/2023_03_27_081716_create_applications_table.php index b408589d2..3badbe69a 100644 --- a/database/migrations/2023_03_27_081716_create_applications_table.php +++ b/database/migrations/2023_03_27_081716_create_applications_table.php @@ -13,6 +13,7 @@ public function up(): void { Schema::create('applications', function (Blueprint $table) { $table->id(); + $table->integer('project_id')->nullable(); $table->string('uuid')->unique(); $table->string('name'); diff --git a/database/migrations/2023_05_09_093548_create_github_events_applications_table.php b/database/migrations/2023_05_09_093548_create_github_events_applications_table.php new file mode 100644 index 000000000..f2c2d52d8 --- /dev/null +++ b/database/migrations/2023_05_09_093548_create_github_events_applications_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('delivery_guid'); + $table->foreignId('application_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('github_events_applications'); + } +}; diff --git a/database/seeders/ApplicationSeeder.php b/database/seeders/ApplicationSeeder.php index 2c6fb736e..78f7b63fe 100644 --- a/database/seeders/ApplicationSeeder.php +++ b/database/seeders/ApplicationSeeder.php @@ -25,6 +25,7 @@ public function run(): void Application::create([ 'name' => 'coollabsio/coolify-examples:nodejs-fastify', + 'project_id' => 603035348, 'git_repository' => 'coollabsio/coolify-examples', 'git_branch' => 'nodejs-fastify', 'build_pack' => 'nixpacks', diff --git a/database/seeders/GithubAppSeeder.php b/database/seeders/GithubAppSeeder.php index 8a980b91c..a9b8dbbc4 100644 --- a/database/seeders/GithubAppSeeder.php +++ b/database/seeders/GithubAppSeeder.php @@ -26,7 +26,8 @@ public function run(): void 'team_id' => $root_team->id, ]); GithubApp::create([ - 'name' => 'coolify-laravel-development-private-github', + 'name' => 'coolify-laravel-development-public', + 'uuid' => '69420', 'api_url' => 'https://api.github.com', 'html_url' => 'https://github.com', 'is_public' => false, diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 830296458..df63a074c 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -1,5 +1,5 @@ -
v{{ config('coolify.version') }}
+
v{{ config('version') }}
Login @if ($is_registration_enabled) Register diff --git a/routes/api.php b/routes/api.php index 07528bf57..7005837de 100644 --- a/routes/api.php +++ b/routes/api.php @@ -18,13 +18,6 @@ Route::get('/health', function () { return 'OK'; }); -Route::get('/webhooks/source/github/redirect', function () { - $code = request()->get('code'); - $state = request()->get('state'); - $github_app = GithubApp::where('uuid', $state)->firstOrFail(); - return 'OK'; -}); - // Route::middleware('auth:sanctum')->get('/user', function (Request $request) { // return $request->user(); // }); diff --git a/routes/web.php b/routes/web.php index d0910b0a8..b3b5e02d1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -38,7 +38,6 @@ })->flatten(); $private_keys = PrivateKey::where('team_id', $id)->get(); $github_apps = GithubApp::private(); - return view('dashboard', [ 'servers' => $servers->sortBy('name'), 'projects' => $projects->sortBy('name'), @@ -86,7 +85,7 @@ Route::get('/source/new', fn () => view('source.new'))->name('source.new'); Route::get('/source/github/{github_app_uuid}', function (Request $request) { $github_app = GithubApp::where('uuid', request()->github_app_uuid)->first(); - $name = Str::kebab('coolify' . $github_app->name); + $name = Str::of(Str::kebab($github_app->name))->start('coolify-'); $settings = InstanceSettings::first(); $host = $request->schemeAndHttpHost(); if ($settings->fqdn) { diff --git a/routes/webhooks.php b/routes/webhooks.php index 54d51dfb7..25d4d4f1d 100644 --- a/routes/webhooks.php +++ b/routes/webhooks.php @@ -1,9 +1,14 @@ 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 = request()->header('X-Hub-Signature-256'); + $payload = request()->collect(); + if ($x_github_event === 'ping') { + // Just pong + return response('pong'); + } + if ($x_github_event === 'installation') { + // Installation handled by setup redirect url. Repositories queried on-demand. + return response('cool'); + } + $github_app = GithubApp::where('app_id', $x_github_hook_installation_target_id)->firstOrFail(); + // TODO: Verify signature + // $webhook_secret = data_get($github_app, 'webhook_secret'); + // $key = hash('sha256', $webhook_secret, true); + // $hmac = hash_hmac('sha256', request()->getContent(), $key); + // if (!hash_equals($hmac, $x_hub_signature_256)) { + // return response('not 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/'); + } + } + if ($x_github_event === 'pull_request') { + $id = data_get($payload, 'pull_request.base.repo.id'); + $branch = data_get($payload, 'pull_request.base.ref'); + } + if (!$id || !$branch) { + return response('not cool'); + } + $applications = Application::where('project_id', $id)->where('git_branch', $branch)->get(); + foreach ($applications as $application) { + GithubEventsApplications::create([ + "delivery_guid" => $x_github_delivery, + "application_id" => $application->id + ]); + $deployment_uuid = new Cuid2(7); + dispatch(new DeployApplicationJob( + deployment_uuid: $deployment_uuid, + application_uuid: $application->uuid, + force_rebuild: false, + )); + } + } catch (\Exception $e) { + return generalErrorHandler($e); + } +}); diff --git a/thunder-tests/thunderCollection.json b/thunder-tests/thunderCollection.json index 0637a088a..fbb15d33a 100644 --- a/thunder-tests/thunderCollection.json +++ b/thunder-tests/thunderCollection.json @@ -1 +1,17 @@ -[] \ No newline at end of file +[ + { + "_id": "e6458286-eef1-401c-be84-860b111d66f0", + "colName": "Webhooks", + "created": "2023-05-09T11:45:36.504Z", + "sortNum": 10000, + "folders": [ + { + "_id": "b8cfd093-5467-44a2-9221-ad0207717310", + "name": "GitHub", + "containerId": "", + "created": "2023-05-09T11:45:40.630Z", + "sortNum": 10000 + } + ] + } +] \ No newline at end of file diff --git a/thunder-tests/thunderEnvironment.json b/thunder-tests/thunderEnvironment.json index 0637a088a..a6c69759e 100644 --- a/thunder-tests/thunderEnvironment.json +++ b/thunder-tests/thunderEnvironment.json @@ -1 +1,13 @@ -[] \ No newline at end of file +[ + { + "_id": "bd3f3c2e-b161-40c9-9077-efcf40dfe1f4", + "name": "(Local Env)", + "default": false, + "global": true, + "local": true, + "sortNum": -1, + "created": "2023-05-09T11:48:22.144Z", + "modified": "2023-05-09T11:48:22.144Z", + "data": [] + } +] \ No newline at end of file diff --git a/thunder-tests/thunderclient.json b/thunder-tests/thunderclient.json index 0637a088a..c8a4faa51 100644 --- a/thunder-tests/thunderclient.json +++ b/thunder-tests/thunderclient.json @@ -1 +1,50 @@ -[] \ No newline at end of file +[ + { + "_id": "b3d379ab-e5e4-4ba4-991d-b6c8c6bbcb98", + "colId": "e6458286-eef1-401c-be84-860b111d66f0", + "containerId": "b8cfd093-5467-44a2-9221-ad0207717310", + "name": "Push", + "url": "http://localhost:8000/webhooks/source/github/events", + "method": "POST", + "sortNum": 10000, + "created": "2023-05-09T11:45:50.227Z", + "modified": "2023-05-09T12:22:27.192Z", + "headers": [ + { + "name": "X-GitHub-Delivery", + "value": "9b4bc300-ee63-11ed-9133-5f71dd83487d" + }, + { + "name": "X-GitHub-Event", + "value": "push" + }, + { + "name": "X-GitHub-Hook-ID", + "value": "400873078" + }, + { + "name": "X-GitHub-Hook-Installation-Target-ID", + "value": "292941" + }, + { + "name": "X-GitHub-Hook-Installation-Target-Type", + "value": "integration" + }, + { + "name": "X-Hub-Signature-256", + "value": "sha256=d5c8d05cc6de14422ab3661d37ec4b98e71f4fdd63d1116f5dedfcb0213ee03d" + }, + { + "name": "Content-Type", + "value": "application/json" + } + ], + "params": [], + "body": { + "type": "json", + "raw": "{\n \"ref\": \"{{repository_ref}}\",\n \"repository\": {\n \"id\": \"{{repository_id}}\",\n \"full_name\": \"{{repository_name}}\"\n }\n}", + "form": [] + }, + "tests": [] + } +] \ No newline at end of file