fix bitbucket (#119)

Fix Bitbucket API errors
This commit is contained in:
Saeed Vaziry 2024-03-17 11:36:04 +01:00 committed by GitHub
parent f0c4fc4812
commit b07ae470f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 167 additions and 45 deletions

View File

@ -13,13 +13,17 @@ class ConnectSourceControl
public function connect(array $input): void public function connect(array $input): void
{ {
$this->validate($input); $this->validate($input);
$sourceControl = new SourceControl([ $sourceControl = new SourceControl([
'provider' => $input['provider'], 'provider' => $input['provider'],
'profile' => $input['name'], 'profile' => $input['name'],
'access_token' => $input['token'],
'url' => Arr::has($input, 'url') ? $input['url'] : null, 'url' => Arr::has($input, 'url') ? $input['url'] : null,
]); ]);
$this->validateProvider($sourceControl, $input);
$sourceControl->provider_data = $sourceControl->provider()->createData($input);
if (! $sourceControl->provider()->connect()) { if (! $sourceControl->provider()->connect()) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider] 'token' => __('Cannot connect to :provider or invalid token!', ['provider' => $sourceControl->provider]
@ -43,15 +47,15 @@ private function validate(array $input): void
'name' => [ 'name' => [
'required', 'required',
], ],
'token' => [
'required',
],
'url' => [
'nullable',
'url:http,https',
'ends_with:/',
],
]; ];
Validator::make($input, $rules)->validate(); Validator::make($input, $rules)->validate();
} }
/**
* @throws ValidationException
*/
private function validateProvider(SourceControl $sourceControl, array $input): void
{
Validator::make($input, $sourceControl->provider()->createRules($input))->validate();
}
} }

View File

@ -8,6 +8,7 @@
/** /**
* @property string $provider * @property string $provider
* @property array $provider_data
* @property ?string $profile * @property ?string $profile
* @property ?string $url * @property ?string $url
* @property string $access_token * @property string $access_token
@ -18,6 +19,7 @@ class SourceControl extends AbstractModel
protected $fillable = [ protected $fillable = [
'provider', 'provider',
'provider_data',
'profile', 'profile',
'url', 'url',
'access_token', 'access_token',
@ -25,6 +27,7 @@ class SourceControl extends AbstractModel
protected $casts = [ protected $casts = [
'access_token' => 'encrypted', 'access_token' => 'encrypted',
'provider_data' => 'encrypted:array',
]; ];
public function provider(): SourceControlProvider public function provider(): SourceControlProvider

View File

@ -17,6 +17,35 @@ public function __construct(SourceControl $sourceControl)
$this->sourceControl = $sourceControl; $this->sourceControl = $sourceControl;
} }
public function createRules(array $input): array
{
return [
'token' => 'required',
'url' => [
'nullable',
'url:http,https',
'ends_with:/',
],
];
}
public function createData(array $input): array
{
return [
'token' => $input['token'] ?? '',
];
}
public function data(): array
{
// support for older data
$token = $this->sourceControl->access_token ?? '';
return [
'token' => $this->sourceControl->provider_data['token'] ?? $token,
];
}
/** /**
* @throws SourceControlIsNotConnected * @throws SourceControlIsNotConnected
* @throws RepositoryNotFound * @throws RepositoryNotFound

View File

@ -13,9 +13,33 @@ class Bitbucket extends AbstractSourceControlProvider
{ {
protected string $apiUrl = 'https://api.bitbucket.org/2.0'; protected string $apiUrl = 'https://api.bitbucket.org/2.0';
public function createRules(array $input): array
{
return [
'username' => 'required',
'password' => 'required',
];
}
public function createData(array $input): array
{
return [
'username' => $input['username'] ?? '',
'password' => $input['password'] ?? '',
];
}
public function data(): array
{
return [
'username' => $this->sourceControl->provider_data['username'] ?? '',
'password' => $this->sourceControl->provider_data['password'] ?? '',
];
}
public function connect(): bool public function connect(): bool
{ {
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withHeaders($this->getAuthenticationHeaders())
->get($this->apiUrl.'/repositories'); ->get($this->apiUrl.'/repositories');
return $res->successful(); return $res->successful();
@ -26,7 +50,7 @@ public function connect(): bool
*/ */
public function getRepo(?string $repo = null): mixed public function getRepo(?string $repo = null): mixed
{ {
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withHeaders($this->getAuthenticationHeaders())
->get($this->apiUrl."/repositories/$repo"); ->get($this->apiUrl."/repositories/$repo");
$this->handleResponseErrors($res, $repo); $this->handleResponseErrors($res, $repo);
@ -44,14 +68,15 @@ public function fullRepoUrl(string $repo, string $key): string
*/ */
public function deployHook(string $repo, array $events, string $secret): array public function deployHook(string $repo, array $events, string $secret): array
{ {
$response = Http::withToken($this->sourceControl->access_token)->post($this->apiUrl."/repositories/$repo/hooks", [ $response = Http::withHeaders($this->getAuthenticationHeaders())
'description' => 'deploy', ->post($this->apiUrl."/repositories/$repo/hooks", [
'url' => url('/api/git-hooks?secret='.$secret), 'description' => 'deploy',
'events' => [ 'url' => url('/api/git-hooks?secret='.$secret),
'repo:'.implode(',', $events), 'events' => [
], 'repo:'.implode(',', $events),
'active' => true, ],
]); 'active' => true,
]);
if ($response->status() != 201) { if ($response->status() != 201) {
throw new FailedToDeployGitHook($response->json()['error']['message']); throw new FailedToDeployGitHook($response->json()['error']['message']);
@ -69,7 +94,8 @@ public function deployHook(string $repo, array $events, string $secret): array
public function destroyHook(string $repo, string $hookId): void public function destroyHook(string $repo, string $hookId): void
{ {
$hookId = urlencode($hookId); $hookId = urlencode($hookId);
$response = Http::withToken($this->sourceControl->access_token)->delete($this->apiUrl."/repositories/$repo/hooks/$hookId"); $response = Http::withHeaders($this->getAuthenticationHeaders())
->delete($this->apiUrl."/repositories/$repo/hooks/$hookId");
if ($response->status() != 204) { if ($response->status() != 204) {
throw new FailedToDestroyGitHook('Error'); throw new FailedToDestroyGitHook('Error');
@ -81,7 +107,7 @@ public function destroyHook(string $repo, string $hookId): void
*/ */
public function getLastCommit(string $repo, string $branch): ?array public function getLastCommit(string $repo, string $branch): ?array
{ {
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withHeaders($this->getAuthenticationHeaders())
->get($this->apiUrl."/repositories/$repo/commits?include=".$branch); ->get($this->apiUrl."/repositories/$repo/commits?include=".$branch);
$this->handleResponseErrors($res, $repo); $this->handleResponseErrors($res, $repo);
@ -108,7 +134,7 @@ public function getLastCommit(string $repo, string $branch): ?array
*/ */
public function deployKey(string $title, string $repo, string $key): void public function deployKey(string $title, string $repo, string $key): void
{ {
$res = Http::withToken($this->sourceControl->access_token)->post( $res = Http::withHeaders($this->getAuthenticationHeaders())->post(
$this->apiUrl."/repositories/$repo/deploy-keys", $this->apiUrl."/repositories/$repo/deploy-keys",
[ [
'label' => $title, 'label' => $title,
@ -116,7 +142,7 @@ public function deployKey(string $title, string $repo, string $key): void
] ]
); );
if ($res->status() != 201) { if ($res->status() != 200) {
throw new FailedToDeployGitKey($res->json()['error']['message']); throw new FailedToDeployGitKey($res->json()['error']['message']);
} }
} }
@ -130,4 +156,15 @@ protected function getCommitter(string $raw): array
'email' => Str::replace('>', '', $committer[1]), 'email' => Str::replace('>', '', $committer[1]),
]; ];
} }
private function getAuthenticationHeaders(): array
{
$username = $this->data()['username'];
$password = $this->data()['password'];
$basicAuth = base64_encode("$username:$password");
return [
'Authorization' => 'Basic '.$basicAuth,
];
}
} }

View File

@ -16,7 +16,7 @@ public function connect(): bool
{ {
$res = Http::withHeaders([ $res = Http::withHeaders([
'Accept' => 'application/vnd.github.v3+json', 'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'Bearer '.$this->sourceControl->access_token, 'Authorization' => 'Bearer '.$this->data()['token'],
])->get($this->apiUrl.'/user/repos'); ])->get($this->apiUrl.'/user/repos');
return $res->successful(); return $res->successful();
@ -34,7 +34,7 @@ public function getRepo(?string $repo = null): mixed
} }
$res = Http::withHeaders([ $res = Http::withHeaders([
'Accept' => 'application/vnd.github.v3+json', 'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'Bearer '.$this->sourceControl->access_token, 'Authorization' => 'Bearer '.$this->data()['token'],
])->get($url); ])->get($url);
$this->handleResponseErrors($res, $repo); $this->handleResponseErrors($res, $repo);
@ -54,7 +54,7 @@ public function deployHook(string $repo, array $events, string $secret): array
{ {
$response = Http::withHeaders([ $response = Http::withHeaders([
'Accept' => 'application/vnd.github.v3+json', 'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'Bearer '.$this->sourceControl->access_token, 'Authorization' => 'Bearer '.$this->data()['token'],
])->post($this->apiUrl."/repos/$repo/hooks", [ ])->post($this->apiUrl."/repos/$repo/hooks", [
'name' => 'web', 'name' => 'web',
'events' => $events, 'events' => $events,
@ -82,7 +82,7 @@ public function destroyHook(string $repo, string $hookId): void
{ {
$response = Http::withHeaders([ $response = Http::withHeaders([
'Accept' => 'application/vnd.github.v3+json', 'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'Bearer '.$this->sourceControl->access_token, 'Authorization' => 'Bearer '.$this->data()['token'],
])->delete($this->apiUrl."/repos/$repo/hooks/$hookId"); ])->delete($this->apiUrl."/repos/$repo/hooks/$hookId");
if ($response->status() != 204) { if ($response->status() != 204) {
@ -98,7 +98,7 @@ public function getLastCommit(string $repo, string $branch): ?array
$url = $this->apiUrl.'/repos/'.$repo.'/commits/'.$branch; $url = $this->apiUrl.'/repos/'.$repo.'/commits/'.$branch;
$res = Http::withHeaders([ $res = Http::withHeaders([
'Accept' => 'application/vnd.github.v3+json', 'Accept' => 'application/vnd.github.v3+json',
'Authorization' => 'Bearer '.$this->sourceControl->access_token, 'Authorization' => 'Bearer '.$this->data()['token'],
])->get($url); ])->get($url);
$this->handleResponseErrors($res, $repo); $this->handleResponseErrors($res, $repo);
@ -124,7 +124,7 @@ public function getLastCommit(string $repo, string $branch): ?array
*/ */
public function deployKey(string $title, string $repo, string $key): void public function deployKey(string $title, string $repo, string $key): void
{ {
$response = Http::withToken($this->sourceControl->access_token)->post( $response = Http::withToken($this->data()['token'])->post(
$this->apiUrl.'/repos/'.$repo.'/keys', $this->apiUrl.'/repos/'.$repo.'/keys',
[ [
'title' => $title, 'title' => $title,

View File

@ -16,7 +16,7 @@ class Gitlab extends AbstractSourceControlProvider
public function connect(): bool public function connect(): bool
{ {
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->data()['token'])
->get($this->getApiUrl().'/projects'); ->get($this->getApiUrl().'/projects');
return $res->successful(); return $res->successful();
@ -28,7 +28,7 @@ public function connect(): bool
public function getRepo(?string $repo = null): mixed public function getRepo(?string $repo = null): mixed
{ {
$repository = $repo ? urlencode($repo) : null; $repository = $repo ? urlencode($repo) : null;
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->data()['token'])
->get($this->getApiUrl().'/projects/'.$repository.'/repository/commits'); ->get($this->getApiUrl().'/projects/'.$repository.'/repository/commits');
$this->handleResponseErrors($res, $repo); $this->handleResponseErrors($res, $repo);
@ -49,7 +49,7 @@ public function fullRepoUrl(string $repo, string $key): string
public function deployHook(string $repo, array $events, string $secret): array public function deployHook(string $repo, array $events, string $secret): array
{ {
$repository = urlencode($repo); $repository = urlencode($repo);
$response = Http::withToken($this->sourceControl->access_token)->post( $response = Http::withToken($this->data()['token'])->post(
$this->getApiUrl().'/projects/'.$repository.'/hooks', $this->getApiUrl().'/projects/'.$repository.'/hooks',
[ [
'description' => 'deploy', 'description' => 'deploy',
@ -84,7 +84,7 @@ public function deployHook(string $repo, array $events, string $secret): array
public function destroyHook(string $repo, string $hookId): void public function destroyHook(string $repo, string $hookId): void
{ {
$repository = urlencode($repo); $repository = urlencode($repo);
$response = Http::withToken($this->sourceControl->access_token)->delete( $response = Http::withToken($this->data()['token'])->delete(
$this->getApiUrl().'/projects/'.$repository.'/hooks/'.$hookId $this->getApiUrl().'/projects/'.$repository.'/hooks/'.$hookId
); );
@ -99,7 +99,7 @@ public function destroyHook(string $repo, string $hookId): void
public function getLastCommit(string $repo, string $branch): ?array public function getLastCommit(string $repo, string $branch): ?array
{ {
$repository = urlencode($repo); $repository = urlencode($repo);
$res = Http::withToken($this->sourceControl->access_token) $res = Http::withToken($this->data()['token'])
->get($this->getApiUrl().'/projects/'.$repository.'/repository/commits?ref_name='.$branch); ->get($this->getApiUrl().'/projects/'.$repository.'/repository/commits?ref_name='.$branch);
$this->handleResponseErrors($res, $repo); $this->handleResponseErrors($res, $repo);
@ -126,7 +126,7 @@ public function getLastCommit(string $repo, string $branch): ?array
public function deployKey(string $title, string $repo, string $key): void public function deployKey(string $title, string $repo, string $key): void
{ {
$repository = urlencode($repo); $repository = urlencode($repo);
$response = Http::withToken($this->sourceControl->access_token)->post( $response = Http::withToken($this->data()['token'])->post(
$this->getApiUrl().'/projects/'.$repository.'/deploy_keys', $this->getApiUrl().'/projects/'.$repository.'/deploy_keys',
[ [
'title' => $title, 'title' => $title,

View File

@ -4,6 +4,12 @@
interface SourceControlProvider interface SourceControlProvider
{ {
public function createRules(array $input): array;
public function createData(array $input): array;
public function data(): array;
public function connect(): bool; public function connect(): bool;
public function getRepo(?string $repo = null): mixed; public function getRepo(?string $repo = null): mixed;

View File

@ -11,7 +11,8 @@ public function up(): void
Schema::create('source_controls', function (Blueprint $table) { Schema::create('source_controls', function (Blueprint $table) {
$table->id(); $table->id();
$table->string('provider'); $table->string('provider');
$table->longText('access_token'); $table->json('provider_data')->nullable()->after('provider');
$table->longText('access_token')->nullable(); // @TODO: remove this column
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@ -69,7 +69,7 @@ class="mt-1 w-full"
@enderror @enderror
</div> </div>
<div class="mt-6"> <div x-show="['gitlab', 'github'].includes(provider)" class="mt-6">
<x-input-label for="token" value="API Key" /> <x-input-label for="token" value="API Key" />
<x-text-input value="{{ old('token') }}" id="token" name="token" type="text" class="mt-1 w-full" /> <x-text-input value="{{ old('token') }}" id="token" name="token" type="text" class="mt-1 w-full" />
@error("token") @error("token")
@ -77,6 +77,49 @@ class="mt-1 w-full"
@enderror @enderror
</div> </div>
<div x-show="provider === 'bitbucket'">
<div class="mt-6">
<x-input-label for="username" value="Username" />
<x-text-input
value="{{ old('username') }}"
id="username"
name="username"
type="text"
class="mt-1 w-full"
/>
<x-input-help>Your Bitbucket username</x-input-help>
@error("username")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
<div class="mt-6">
<x-input-label for="password" value="Password" />
<x-text-input
value="{{ old('password') }}"
id="password"
name="password"
type="text"
class="mt-1 w-full"
/>
<x-input-help>
Create a new
<a
class="text-primary-500"
href="https://bitbucket.org/account/settings/app-passwords/new"
target="_blank"
>
App Password
</a>
in your Bitbucket account with write and admin access to Workspaces, Projects, Repositories and
Webhooks
</x-input-help>
@error("password")
<x-input-error class="mt-2" :messages="$message" />
@enderror
</div>
</div>
<div class="mt-6 flex justify-end"> <div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')"> <x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Cancel") }} {{ __("Cancel") }}

View File

@ -14,17 +14,16 @@ class SourceControlsTest extends TestCase
/** /**
* @dataProvider data * @dataProvider data
*/ */
public function test_connect_provider(string $provider, ?string $customUrl): void public function test_connect_provider(string $provider, ?string $customUrl, array $input): void
{ {
$this->actingAs($this->user); $this->actingAs($this->user);
Http::fake(); Http::fake();
$input = [ $input = array_merge([
'name' => 'test', 'name' => 'test',
'provider' => $provider, 'provider' => $provider,
'token' => 'token', ], $input);
];
if ($customUrl !== null) { if ($customUrl !== null) {
$input['url'] = $customUrl; $input['url'] = $customUrl;
@ -89,10 +88,10 @@ public function test_cannot_delete_provider(string $provider): void
public static function data(): array public static function data(): array
{ {
return [ return [
['github', null], ['github', null, ['token' => 'test']],
['gitlab', null], ['gitlab', null, ['token' => 'test']],
['gitlab', 'https://git.example.com/'], ['gitlab', 'https://git.example.com/', ['token' => 'test']],
['bitbucket', null], ['bitbucket', null, ['username' => 'test', 'password' => 'test']],
]; ];
} }
} }