mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-07 15:26:48 +08:00
Added the possibility of social provider extension via theme
Also started docs page
This commit is contained in:
parent
9d37af9453
commit
2ae89f2c32
@ -2,21 +2,21 @@
|
|||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Actions\ActivityType;
|
||||||
use BookStack\Auth\SocialAccount;
|
use BookStack\Auth\SocialAccount;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Auth\User;
|
||||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||||
use Laravel\Socialite\Contracts\Provider;
|
use Laravel\Socialite\Contracts\Provider;
|
||||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
|
||||||
class SocialAuthService
|
class SocialAuthService
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $userRepo;
|
|
||||||
protected $socialite;
|
protected $socialite;
|
||||||
protected $socialAccount;
|
protected $socialAccount;
|
||||||
|
|
||||||
@ -25,14 +25,11 @@ class SocialAuthService
|
|||||||
/**
|
/**
|
||||||
* SocialAuthService constructor.
|
* SocialAuthService constructor.
|
||||||
*/
|
*/
|
||||||
public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount)
|
public function __construct(Socialite $socialite)
|
||||||
{
|
{
|
||||||
$this->userRepo = $userRepo;
|
|
||||||
$this->socialite = $socialite;
|
$this->socialite = $socialite;
|
||||||
$this->socialAccount = $socialAccount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the social login path.
|
* Start the social login path.
|
||||||
* @throws SocialDriverNotConfigured
|
* @throws SocialDriverNotConfigured
|
||||||
@ -60,11 +57,11 @@ class SocialAuthService
|
|||||||
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
|
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
|
||||||
{
|
{
|
||||||
// Check social account has not already been used
|
// 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');
|
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();
|
$email = $socialUser->getEmail();
|
||||||
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
|
||||||
}
|
}
|
||||||
@ -91,7 +88,7 @@ class SocialAuthService
|
|||||||
$socialId = $socialUser->getId();
|
$socialId = $socialUser->getId();
|
||||||
|
|
||||||
// Get any attached social accounts or users
|
// 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();
|
$isLoggedIn = auth()->check();
|
||||||
$currentUser = user();
|
$currentUser = user();
|
||||||
$titleCaseDriver = Str::title($socialDriver);
|
$titleCaseDriver = Str::title($socialDriver);
|
||||||
@ -107,8 +104,8 @@ class SocialAuthService
|
|||||||
// When a user is logged in but the social account does not exist,
|
// 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.
|
// Create the social account and attach it to the user & redirect to the profile page.
|
||||||
if ($isLoggedIn && $socialAccount === null) {
|
if ($isLoggedIn && $socialAccount === null) {
|
||||||
$this->fillSocialAccount($socialDriver, $socialUser);
|
$account = $this->newSocialAccount($socialDriver, $socialUser);
|
||||||
$currentUser->socialAccounts()->save($this->socialAccount);
|
$currentUser->socialAccounts()->save($account);
|
||||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
||||||
return redirect($currentUser->getEditUrl());
|
return redirect($currentUser->getEditUrl());
|
||||||
}
|
}
|
||||||
@ -207,19 +204,17 @@ class SocialAuthService
|
|||||||
/**
|
/**
|
||||||
* Fill and return a SocialAccount from the given driver name and SocialUser.
|
* 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' => $socialDriver,
|
||||||
'driver_id' => $socialUser->getId(),
|
'driver_id' => $socialUser->getId(),
|
||||||
'avatar' => $socialUser->getAvatar()
|
'avatar' => $socialUser->getAvatar()
|
||||||
]);
|
]);
|
||||||
return $this->socialAccount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detach a social account from a user.
|
* Detach a social account from a user.
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
|
||||||
*/
|
*/
|
||||||
public function detachSocialAccount(string $socialDriver): void
|
public function detachSocialAccount(string $socialDriver): void
|
||||||
{
|
{
|
||||||
@ -242,4 +237,20 @@ class SocialAuthService
|
|||||||
|
|
||||||
return $driver;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ class SocialController extends Controller
|
|||||||
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
||||||
{
|
{
|
||||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
||||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
|
||||||
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
|
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
|
||||||
|
|
||||||
// Create an array of the user data to create a new user instance
|
// Create an array of the user data to create a new user instance
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php namespace BookStack\Providers;
|
<?php namespace BookStack\Providers;
|
||||||
|
|
||||||
use Blade;
|
use Blade;
|
||||||
|
use BookStack\Auth\Access\SocialAuthService;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Bookshelf;
|
use BookStack\Entities\Models\Bookshelf;
|
||||||
use BookStack\Entities\BreadcrumbsViewComposer;
|
use BookStack\Entities\BreadcrumbsViewComposer;
|
||||||
@ -12,6 +13,7 @@ use Illuminate\Contracts\Cache\Repository;
|
|||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
|
||||||
use Schema;
|
use Schema;
|
||||||
use URL;
|
use URL;
|
||||||
|
|
||||||
@ -62,5 +64,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$this->app->singleton(SettingService::class, function ($app) {
|
$this->app->singleton(SettingService::class, function ($app) {
|
||||||
return new SettingService($app->make(Setting::class), $app->make(Repository::class));
|
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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<?php namespace BookStack\Theming;
|
<?php namespace BookStack\Theming;
|
||||||
|
|
||||||
|
use BookStack\Auth\Access\SocialAuthService;
|
||||||
|
|
||||||
class ThemeService
|
class ThemeService
|
||||||
{
|
{
|
||||||
protected $listeners = [];
|
protected $listeners = [];
|
||||||
@ -47,4 +49,13 @@ class ThemeService
|
|||||||
require $themeActionsFile;
|
require $themeActionsFile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see SocialAuthService::addSocialDriver
|
||||||
|
*/
|
||||||
|
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
|
||||||
|
{
|
||||||
|
$socialAuthService = app()->make(SocialAuthService::class);
|
||||||
|
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler);
|
||||||
|
}
|
||||||
}
|
}
|
22
dev/docs/logic-theme-service.md
Normal file
22
dev/docs/logic-theme-service.md
Normal 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');
|
||||||
|
});
|
||||||
|
```
|
@ -122,6 +122,34 @@ class ThemeTest extends TestCase
|
|||||||
$resp->assertStatus(443);
|
$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)
|
protected function usingThemeFolder(callable $callback)
|
||||||
{
|
{
|
||||||
// Create a folder and configure a theme
|
// Create a folder and configure a theme
|
||||||
|
Loading…
x
Reference in New Issue
Block a user