Skip to content

Commit

Permalink
feat: Add bitbucket connection
Browse files Browse the repository at this point in the history
  • Loading branch information
lewislarsen committed Aug 23, 2024
1 parent 76c826c commit 0b18ae5
Show file tree
Hide file tree
Showing 16 changed files with 403 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ GITHUB_CLIENT_SECRET=
GITLAB_CLIENT_ID=
GITLAB_CLIENT_SECRET=

### BITBUCKET AUTH ###
BITBUCKET_CLIENT_ID=
BITBUCKET_CLIENT_SECRET=

### DEVICE ENDPOINT ###
ENABLE_DEVICE_AUTH_ENDPOINT=false

Expand Down
59 changes: 59 additions & 0 deletions app/Http/Controllers/Connections/BitbucketController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Connections;

use App\Models\UserConnection;
use Exception;
use Illuminate\Http\RedirectResponse;
use Laravel\Socialite\Two\InvalidStateException;
use Symfony\Component\HttpFoundation\RedirectResponse as SymfonyRedirectResponse;

class BitbucketController extends ConnectionsController
{
/** @var string The provider name for Bitbucket */
private const PROVIDER_NAME = UserConnection::PROVIDER_BITBUCKET;

/**
* Redirect the user to Bitbucket's authentication page.
*/
public function redirect(): RedirectResponse|SymfonyRedirectResponse
{
return $this->redirectToProvider(self::PROVIDER_NAME);
}

/**
* Handle the callback from Bitbucket.
*/
public function callback(): RedirectResponse
{
try {
return $this->handleProviderCallback(self::PROVIDER_NAME);
} catch (InvalidStateException) {
return $this->handleInvalidState();
} catch (Exception $e) {
return $this->handleGenericError($e);
}
}

/**
* Handle invalid state exception, which could occur if the user cancels the process.
*/
protected function handleInvalidState(): RedirectResponse
{
return redirect()->route('login')
->with('loginError', 'Bitbucket connection was cancelled or invalid. Please try again if you want to connect your account.');
}

/**
* Handle generic errors during the Bitbucket connection process.
*/
protected function handleGenericError(Exception $exception): RedirectResponse
{
logger()->error('Bitbucket connection error', ['error' => $exception->getMessage()]);

return redirect()->route('login')
->with('loginError', 'An error occurred while connecting to Bitbucket. Please try again later.');
}
}
4 changes: 2 additions & 2 deletions app/Http/Controllers/Connections/GitHubController.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function callback(): RedirectResponse
protected function handleInvalidState(): RedirectResponse
{
return redirect()->route('login')
->with('info', 'GitHub connection was cancelled or invalid. Please try again if you want to connect your account.');
->with('loginError', 'GitHub connection was cancelled or invalid. Please try again if you want to connect your account.');
}

/**
Expand All @@ -51,6 +51,6 @@ protected function handleGenericError(Exception $exception): RedirectResponse
logger()->error('GitHub connection error', ['error' => $exception->getMessage()]);

return redirect()->route('login')
->with('error', 'An error occurred while connecting to GitHub. Please try again later.');
->with('loginError', 'An error occurred while connecting to GitHub. Please try again later.');
}
}
4 changes: 2 additions & 2 deletions app/Http/Controllers/Connections/GitLabController.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function callback(): RedirectResponse
protected function handleInvalidState(): RedirectResponse
{
return redirect()->route('login')
->with('info', 'GitLab connection was cancelled or invalid. Please try again if you want to connect your account.');
->with('loginError', 'GitLab connection was cancelled or invalid. Please try again if you want to connect your account.');
}

/**
Expand All @@ -51,6 +51,6 @@ protected function handleGenericError(Exception $exception): RedirectResponse
logger()->error('GitLab connection error', ['error' => $exception->getMessage()]);

return redirect()->route('login')
->with('error', 'An error occurred while connecting to GitLab. Please try again later.');
->with('loginError', 'An error occurred while connecting to GitLab. Please try again later.');
}
}
1 change: 1 addition & 0 deletions app/Livewire/Profile/ConnectionsPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function connect(string $provider): void
$route = match ($provider) {
'github' => 'github.redirect',
'gitlab' => 'gitlab.redirect',
'bitbucket' => 'bitbucket.redirect',
default => null,
};

Expand Down
13 changes: 13 additions & 0 deletions app/Models/UserConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ class UserConnection extends Model
*/
public const string PROVIDER_GITLAB = 'gitlab';

/**
* Provider name for Bitbucket connections.
*/
public const string PROVIDER_BITBUCKET = 'bitbucket';

/**
* The attributes that aren't mass assignable.
*
Expand Down Expand Up @@ -63,6 +68,14 @@ public function isGitLab(): bool
return $this->provider_name === self::PROVIDER_GITLAB;
}

/**
* Determine if the connection is for GitLab.
*/
public function isBitbucket(): bool
{
return $this->provider_name === self::PROVIDER_BITBUCKET;
}

/**
* The attributes that should be cast.
*
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"livewire/volt": "^1.0",
"masmerise/livewire-toaster": "^2.2",
"phpseclib/phpseclib": "~3.0",
"socialiteproviders/bitbucket": "^4.1",
"spatie/laravel-flare": "^1.0"
},
"require-dev": {
Expand Down
117 changes: 116 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@
'client_secret' => env('GITLAB_CLIENT_SECRET'),
'redirect' => config('app.url') . '/auth/gitlab/callback',
],

'bitbucket' => [
'client_id' => env('BITBUCKET_CLIENT_ID'),
'client_secret' => env('BITBUCKET_CLIENT_SECRET'),
'redirect' => config('app.url') . '/auth/bitbucket/callback',
],
];
11 changes: 11 additions & 0 deletions database/factories/UserConnectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function definition(): array
'provider_name' => $this->faker->randomElement([
UserConnection::PROVIDER_GITHUB,
UserConnection::PROVIDER_GITLAB,
UserConnection::PROVIDER_BITBUCKET,
]),
'provider_user_id' => $this->faker->uuid,
'provider_email' => $this->faker->safeEmail,
Expand Down Expand Up @@ -55,4 +56,14 @@ public function gitlab(): self
'provider_name' => UserConnection::PROVIDER_GITLAB,
]);
}

/**
* Indicate that the connection is for Bitbucket.
*/
public function bitbucket(): self
{
return $this->state(fn (array $attributes) => [
'provider_name' => UserConnection::PROVIDER_BITBUCKET,
]);
}
}
4 changes: 4 additions & 0 deletions resources/views/components/icons/bitbucket.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<svg fill="#1A74ED" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {{ $attributes->merge() }}><title>Bitbucket</title>
<path
d="M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891zM14.52 15.53H9.522L8.17 8.466h7.561z"/>
</svg>
17 changes: 17 additions & 0 deletions resources/views/livewire/pages/auth/login.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,23 @@ public function login(): void
</div>
@endif

@if (config('services.bitbucket.client_id') && config('services.bitbucket.client_secret'))
<div class="mt-6">
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300 dark:border-gray-700"></div>
</div>
</div>

<div class="mt-6">
<a href="{{ route('bitbucket.redirect') }}" class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-700">
<x-icons.bitbucket class="w-5 h-5 mr-3"/>
<span>{{ __('Login with Bitbucket') }}</span>
</a>
</div>
</div>
@endif

<div class="text-center mt-8">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-4">
{{ __('By creating an account, you agree to our Terms of Service and our Privacy Policy.') }}
Expand Down
47 changes: 47 additions & 0 deletions resources/views/livewire/profile/connections-page.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,53 @@ class="w-full sm:w-auto justify-center"
</div>
@endif

@if (config('services.bitbucket.client_id') && config('services.bitbucket.client_secret'))
<!-- Bitbucket Connection -->
<div class="border border-gray-200 dark:border-gray-600 rounded-lg transition-all duration-200 overflow-hidden">
<div class="p-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center mb-4 sm:mb-0">
<div class="flex-shrink-0 mr-4">
<svg class="h-10 w-10 text-gray-500 dark:text-gray-400" fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bitbucket</title><path d="M.778 1.213a.768.768 0 00-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 00.77-.646l3.27-20.03a.768.768 0 00-.768-.891zM14.52 15.53H9.522L8.17 8.466h7.561z"/></svg>
</div>
<div>
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">{{ __('Bitbucket') }}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">{{ __('Attach your Bitbucket account to unlock additional features.') }}</p>
</div>
</div>
<div class="flex justify-end sm:ml-4 sm:flex-shrink-0 space-x-2">
@if ($this->isConnected('bitbucket'))
@if ($this->hasRefreshToken('bitbucket'))
<x-secondary-button
wire:click="refresh('bitbucket')"
wire:loading.attr="disabled"
class="w-full sm:w-auto justify-center"
>
{{ __('Refresh Token') }}
</x-secondary-button>
@endif
<x-danger-button
wire:click="disconnect('bitbucket')"
wire:loading.attr="disabled"
class="w-full sm:w-auto justify-center"
>
{{ __('Disconnect') }}
</x-danger-button>
@else
<x-secondary-button
wire:click="connect('bitbucket')"
wire:loading.attr="disabled"
class="w-full sm:w-auto justify-center"
>
{{ __('Connect') }}
</x-secondary-button>
@endif
</div>
</div>
</div>
</div>
@endif

<!-- You can add more connections here by duplicating the above structure -->

</div>
Expand Down
Loading

0 comments on commit 0b18ae5

Please sign in to comment.