Added the possibility of social provider extension via theme

Also started docs page
This commit is contained in:
Dan Brown 2021-03-19 16:16:26 +00:00
parent 9d37af9453
commit 2ae89f2c32
6 changed files with 95 additions and 17 deletions

View File

@ -2,21 +2,21 @@
use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\UserRepo;
use BookStack\Auth\User;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Activity;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\Factory as Socialite;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Contracts\User as SocialUser;
use SocialiteProviders\Manager\SocialiteWasCalled;
use Symfony\Component\HttpFoundation\RedirectResponse;
class SocialAuthService
{
protected $userRepo;
protected $socialite;
protected $socialAccount;
@ -25,14 +25,11 @@ class SocialAuthService
/**
* SocialAuthService constructor.
*/
public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount)
public function __construct(Socialite $socialite)
{
$this->userRepo = $userRepo;
$this->socialite = $socialite;
$this->socialAccount = $socialAccount;
}
/**
* Start the social login path.
* @throws SocialDriverNotConfigured
@ -60,11 +57,11 @@ class SocialAuthService
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
{
// Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
if (SocialAccount::query()->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
}
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
$email = $socialUser->getEmail();
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
}
@ -91,7 +88,7 @@ class SocialAuthService
$socialId = $socialUser->getId();
// Get any attached social accounts or users
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
$socialAccount = SocialAccount::query()->where('driver_id', '=', $socialId)->first();
$isLoggedIn = auth()->check();
$currentUser = user();
$titleCaseDriver = Str::title($socialDriver);
@ -107,8 +104,8 @@ class SocialAuthService
// When a user is logged in but the social account does not exist,
// Create the social account and attach it to the user & redirect to the profile page.
if ($isLoggedIn && $socialAccount === null) {
$this->fillSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($this->socialAccount);
$account = $this->newSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($account);
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
return redirect($currentUser->getEditUrl());
}
@ -207,19 +204,17 @@ class SocialAuthService
/**
* Fill and return a SocialAccount from the given driver name and SocialUser.
*/
public function fillSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
{
$this->socialAccount->fill([
return new SocialAccount([
'driver' => $socialDriver,
'driver_id' => $socialUser->getId(),
'avatar' => $socialUser->getAvatar()
]);
return $this->socialAccount;
}
/**
* Detach a social account from a user.
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function detachSocialAccount(string $socialDriver): void
{
@ -242,4 +237,20 @@ class SocialAuthService
return $driver;
}
/**
* Add a custom socialite driver to be used.
* Driver name should be lower_snake_case.
* Config array should mirror the structure of a service
* within the `Config/services.php` file.
* Handler should be a Class@method handler to the SocialiteWasCalled event.
*/
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
{
$this->validSocialDrivers[] = $driverName;
config()->set('services.' . $driverName, $config);
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
}
}

View File

@ -110,7 +110,7 @@ class SocialController extends Controller
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
// Create an array of the user data to create a new user instance

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Providers;
use Blade;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\BreadcrumbsViewComposer;
@ -12,6 +13,7 @@ use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
use Schema;
use URL;
@ -62,5 +64,9 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton(SettingService::class, function ($app) {
return new SettingService($app->make(Setting::class), $app->make(Repository::class));
});
$this->app->singleton(SocialAuthService::class, function($app) {
return new SocialAuthService($app->make(SocialiteFactory::class));
});
}
}

View File

@ -1,5 +1,7 @@
<?php namespace BookStack\Theming;
use BookStack\Auth\Access\SocialAuthService;
class ThemeService
{
protected $listeners = [];
@ -47,4 +49,13 @@ class ThemeService
require $themeActionsFile;
}
}
/**
* @see SocialAuthService::addSocialDriver
*/
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
{
$socialAuthService = app()->make(SocialAuthService::class);
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler);
}
}

View File

@ -0,0 +1,22 @@
# Logic Theme Service
#### Custom Socialite Service Example
The below shows an example of adding a custom reddit socialite service to BookStack.
BookStack exposes a helper function for this via `Theme::addSocialDriver` which sets the required config and event listeners in the platform.
The require statements reference composer installed dependencies within the theme folder. They are required manually since they are not auto-loaded like other app files due to being outside the main BookStack dependency list.
```php
require "vendor/socialiteproviders/reddit/Provider.php";
require "vendor/socialiteproviders/reddit/RedditExtendSocialite.php";
Theme::listen(ThemeEvents::APP_BOOT, function($app) {
Theme::addSocialDriver('reddit', [
'client_id' => 'abc123',
'client_secret' => 'def456789',
'name' => 'Reddit',
], '\SocialiteProviders\Reddit\RedditExtendSocialite@handle');
});
```

View File

@ -122,6 +122,34 @@ class ThemeTest extends TestCase
$resp->assertStatus(443);
}
public function test_add_social_driver()
{
Theme::addSocialDriver('catnet', [
'client_id' => 'abc123',
'client_secret' => 'def456'
], 'SocialiteProviders\Discord\DiscordExtendSocialite@handleTesting');
$this->assertEquals('catnet', config('services.catnet.name'));
$this->assertEquals('abc123', config('services.catnet.client_id'));
$this->assertEquals(url('/login/service/catnet/callback'), config('services.catnet.redirect'));
$loginResp = $this->get('/login');
$loginResp->assertSee('login/service/catnet');
}
public function test_add_social_driver_uses_name_in_config_if_given()
{
Theme::addSocialDriver('catnet', [
'client_id' => 'abc123',
'client_secret' => 'def456',
'name' => 'Super Cat Name',
], 'SocialiteProviders\Discord\DiscordExtendSocialite@handleTesting');
$this->assertEquals('Super Cat Name', config('services.catnet.name'));
$loginResp = $this->get('/login');
$loginResp->assertSee('Super Cat Name');
}
protected function usingThemeFolder(callable $callback)
{
// Create a folder and configure a theme