Skip to content

Commit

Permalink
Merge branch 'vatger' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
paulhollmann committed Dec 20, 2023
2 parents 5d08f7c + d9ffc33 commit 61ed38f
Show file tree
Hide file tree
Showing 13 changed files with 402 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ webpack-stats.json
.phpunit.result.cache
.DS_Store
phpstan.neon
esbuild-meta.json
composer.lock
esbuild-meta.json
189 changes: 189 additions & 0 deletions app/Access/Controllers/VATSIMConnectController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php


namespace BookStack\Access\Controllers;

use BookStack\App\Providers\ConnectProvider;
use BookStack\Http\Controller;
use BookStack\Users\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Str;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use UnexpectedValueException;

class VATSIMConnectController extends Controller
{
/**
* The VATSIM Authentication Provider Instance
*/
protected ConnectProvider $_provider;

/**
* Initialize the Controller with a new ConnectProvider instance
*/
function __construct()
{
$this->_provider = new ConnectProvider();
}

/**
* Authentication entrypoint
* This function will handle the state and request of an authentication attempt
*/
public function login(Request $request)
{

// Use this line only if you want to test the "LOCAL" backup authentication
// while developing. NEVER USE IT IN PRODUCTION
// if(config('app.env') !== 'production') return redirect()->route('vatsim.authentication.connect.local');

// Initiation state is without 'code' and 'state'
if (!$request->has('code') || !$request->has('state')) {
try {
// Initiate the authentication process using VATSIM Connect
// 1. Test if the service is available at all.
// 2. If available: Prepare the authentication url and send the user away to it
$response = \Illuminate\Support\Facades\Http::timeout(30)->get(config('vatsim.authentication.connect.base'));
if ($response->status() < 500 || $response->status() > 599) {
$authenticationUrl = $this->_provider->getAuthorizationUrl();
$request->session()->put('vatsim.authentication.connect.state', $this->_provider->getState());
return redirect()->away($authenticationUrl);
} else {
// Send the user to the service unavailable page
Log::info("[ConnectController]::login::response::Status=" . $response->status() . ' ' . $response->reason());
return redirect()->route('vatsim.authentication.connect.failed');
}

} catch (\Illuminate\Http\Client\ConnectionException $ce) {
// Send the user to the service unavailable page
return redirect()->route('vatsim.authentication.connect.failed');
}
} elseif ($request->input('state') !== session()->pull('vatsim.authentication.connect.state')) {
// Within this state there is no state. The only option here is to start again.
$request->session()->invalidate(); // Invalidate and regenerate the session
return redirect()->route('vatsim.authentication.connect.login');
} else {
return $this->_verifyLogin($request);
}
}

/**
* Check that all required data is received from the VATSIM Connect authentication system
*/
protected function _verifyLogin(Request $request)
{
try {
$accessToken = $this->_provider->getAccessToken('authorization_code', [
'code' => $request->input('code')
]);
} catch (UnexpectedValueException $e) {
Log::error("[ConnectController]::_verifyLogin::AccessToken::" . $e->getMessage());
return redirect()->route('vatsim.authentication.connect.failed'); // Wrong format received from the Connect service
} catch (IdentityProviderException $e) {
Log::error("[ConnectController]::_verifyLogin::AccessToken::" . $e->getMessage());
return redirect()->route('vatsim.authentication.connect.failed');
}

try {
$resourceOwner = json_decode(json_encode($this->_provider->getResourceOwner($accessToken)->toArray()));
// $resourceOwner = $this->_provider->getResourceOwner($accessToken);
} catch (UnexpectedValueException $e) {
Log::error("[ConnectController]::_verifyLogin::ResourceOwner::" . $e->getMessage());
return redirect()->route('vatsim.authentication.connect.failed');
}

if (!isset($resourceOwner->data) || !isset($resourceOwner->data->cid) || !isset($resourceOwner->data->personal->name_first) || !isset($resourceOwner->data->personal->name_last) || !isset($resourceOwner->data->personal->email) || $resourceOwner->data->oauth->token_valid !== "true") {
return redirect()->route('vatsim.authentication.connect.failed');
}
// All checks completed. Let's finally sign in the user
$user = $this->_completeLogin($resourceOwner, $accessToken);

Auth::login($user, true);
if ($request->has('email')) {
session()->flashInput([
'email' => $user->email,
]);
}

return redirect()->intended('/');
}

/**
* Complete the authentication process.
*
* @param Object $resourceOwner
* @param Object $accessToken
* @return User
*/
protected function _completeLogin($resourceOwner, $accessToken)
{
$user = User::query()->find($resourceOwner->data->cid);

if (!$user) {
// Create random user slug
$value = $resourceOwner->data->cid;
$slug = Str::slug($value, "-");

// We need to create a new user here
$user = User::query()->create([
'id' => $resourceOwner->data->cid,
'name' => $resourceOwner->data->cid,
'fullname' => $resourceOwner->data->personal->name_first . ' ' . $resourceOwner->data->personal->name_last,
'email' => $resourceOwner->data->personal->email,
'slug' => $slug
]);

// Add role to new user (default role = viewer)
DB::table('role_user')->insert([
'user_id' => $user->id,
'role_id' => 4
]);

// If the user has given us permanent access to the data
if ($resourceOwner->data->oauth->token_valid) {
$user->access_token = $accessToken->getToken();
$user->refresh_token = $accessToken->getRefreshToken();
$user->token_expires = $accessToken->getExpires();
}


} else {
// We know the user exists, so we need to update their account data
$user->update([
'name' => $resourceOwner->data->cid,
'fullname' => $resourceOwner->data->personal->name_first . ' ' . $resourceOwner->data->personal->name_last,
'email' => $resourceOwner->data->personal->email,
]);

//$user->tokens()->delete();

}
//$user->createToken('api-token');

return $user->fresh();
}

/**
* Display a failed message and then return to the login
*
*/
public function failed(): RedirectResponse
{
return redirect('/')->with('error','Error logging in. Please try again.');
}

/**
* End an authenticated session
*/
public function logout(): RedirectResponse
{
Auth::logout();

return redirect()->route('landing')->with('success', 'Logged out successfully.');
}
}
94 changes: 94 additions & 0 deletions app/App/Providers/ConnectProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
namespace BookStack\App\Providers;

use League\OAuth2\Client\Token;
use League\OAuth2\Client\Provider\GenericProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Illuminate\Support\Str;

class ConnectProvider extends GenericProvider
{

/**
* Instance of the provider
* @var GenericProvider
*/
private GenericProvider $_provider;

/**
* The route where we will redirect to after connect sign-on
* @var string
*/
private string $_redirectAtferAuthentication = 'vatsim.authentication.connect.login';

/**
* Force required scopes
* @var bool
*/
private bool $_useRequiredScopes = true;

/**
* Initialize the Provider from configuration
*/
function __construct()
{
parent::__construct(
[
'clientId' => config('vatsim.authentication.connect.id'), // The client ID assigned to you by the provider
'clientSecret' => config('vatsim.authentication.connect.secret'), // The client password assigned to you by the provider
'redirectUri' => route($this->_redirectAtferAuthentication),
'urlAuthorize' => config('vatsim.authentication.connect.base').'/oauth/authorize',
'urlAccessToken' => config('vatsim.authentication.connect.base').'/oauth/token',
'urlResourceOwnerDetails' => config('vatsim.authentication.connect.base').'/api/user',
'scopes' => (Str::contains(config('vatsim.authentication.connect.scopes'), ',')) ? str_replace(',', ' ', config('vatsim.authentication.connect.scopes')) : config('vatsim.authentication.connect.scopes'),
'scopeSeparator' => ' '
]
);
}

/**
* OVERWRITTEN
* Returns authorization parameters based on provided options.
*/
public function getAuthorizationUrl(array $options = [])
{
$base = $this->getBaseAuthorizationUrl();

// injects getDefaultScopes in the initial redirect url as required_scopes
if ($this->_useRequiredScopes){
if (empty($options['required_scopes'])) {
$options['required_scopes'] = $this->getDefaultScopes();
}
if (is_array($options['required_scopes'])) {
$separator = $this->getScopeSeparator();
$options['required_scopes'] = implode($separator, $options['required_scopes']);
}
}
// end

$params = $this->getAuthorizationParameters($options);
$query = $this->getAuthorizationQuery($params);

return $this->appendQuery($base, $query);
}

/**
* Get a new token from an older one
*/
public static function updateToken($token)
{
$c = new ConnectProvider;

try {
return $c->getAccessToken(
'refresh_token',
[
'refresh_token' => $token->getRefreshToken()
]
);
} catch (IdentityProviderException $e) {
return null;
}
}

}
12 changes: 12 additions & 0 deletions app/Config/vatsim.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
return [
'authentication' => [
'connect' => [
'base' => env('VATSIM_OAUTH_BASE', 'https://auth-dev.vatsim.net'),
'id' => env('VATSIM_OAUTH_CLIENT', 0),
'secret' => env('VATSIM_OAUTH_SECRET', ''),
'scopes' => env('VATSIM_OAUTH_SCOPES', 'full_name,email,vatsim_details,country'),
'icon' => env('VATSIM_OAUTH_ICON', 'https://vatsim-forums.nyc3.digitaloceanspaces.com/monthly_2020_08/Vatsim-social_icon.thumb.png.e9bdf49928c9bd5327f08245a68d8304.png'),
]
]
];
14 changes: 7 additions & 7 deletions app/Users/Controllers/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,15 +136,15 @@ public function update(Request $request, int $id)
$this->checkPermission('users-manage');

$validated = $this->validate($request, [
'name' => ['min:2', 'max:100'],
'email' => ['min:2', 'email', 'unique:users,email,' . $id],
'password' => ['required_with:password_confirm', Password::default()],
'password-confirm' => ['same:password', 'required_with:password'],
//'name' => ['min:2', 'max:100'],
//'email' => ['min:2', 'email', 'unique:users,email,' . $id],
//'password' => ['required_with:password_confirm', Password::default()],
//'password-confirm' => ['same:password', 'required_with:password'],
'language' => ['string', 'max:15', 'alpha_dash'],
'roles' => ['array'],
'roles.*' => ['integer'],
'external_auth_id' => ['string'],
'profile_image' => array_merge(['nullable'], $this->getImageValidationRules()),
//'external_auth_id' => ['string'],
//'profile_image' => array_merge(['nullable'], $this->getImageValidationRules()),
]);

$user = $this->userRepo->getById($id);
Expand All @@ -156,7 +156,7 @@ public function update(Request $request, int $id)
$this->imageRepo->destroyImage($user->avatar);
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
$user->image_id = $image->id;
$user->save();
//$user->save();
}

// Delete the profile image if reset option is in request
Expand Down
11 changes: 9 additions & 2 deletions app/Users/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Sanctum\NewAccessToken;
use Laravel\Sanctum\Sanctum;


/**
* Class User.
Expand Down Expand Up @@ -65,7 +70,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*
* @var array
*/
protected $fillable = ['name', 'email'];
protected $fillable = ['id', 'name', 'fullname', 'email', 'slug'];

protected $casts = ['last_activity_at' => 'datetime'];

Expand Down Expand Up @@ -326,6 +331,7 @@ public function getProfileUrl(): string
*/
public function getShortName(int $chars = 8): string
{
/*
if (mb_strlen($this->name) <= $chars) {
return $this->name;
}
Expand All @@ -334,8 +340,9 @@ public function getShortName(int $chars = 8): string
if (mb_strlen($splitName[0]) <= $chars) {
return $splitName[0];
}
*/

return mb_substr($this->name, 0, max($chars - 2, 0)) . '';
return strval($this->id);
}

/**
Expand Down
Loading

0 comments on commit 61ed38f

Please sign in to comment.