Dropbox authorisation now uses a dedicated endpoint on the services controller, and uses OAuth2 state to transfer the storage ID. Added an intermediary screen before authorising. #106

This commit is contained in:
Andy Heathershaw 2020-04-21 08:40:56 +01:00
parent d97b790264
commit f17a84f746
12 changed files with 197 additions and 42 deletions

View File

@ -30,12 +30,17 @@ class ExternalService extends Model
public function hasOAuthStandardOptions()
{
// This list must be mirrored in external_services.js
// This logic must be mirrored in external_services.js
return in_array($this->service_type, [
self::DROPBOX,
self::FACEBOOK,
self::GOOGLE,
self::TWITTER
]);
}
public function isDropbox()
{
// This logic must be mirrored in external_services.js
return $this->service_type == self::DROPBOX;
}
}

View File

@ -7,6 +7,8 @@ use App\Facade\Theme;
use App\Facade\UserConfig;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreServiceRequest;
use App\Services\DropboxService;
use App\Storage;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View;
@ -36,6 +38,53 @@ class ServiceController extends Controller
$this->fieldsToEncrypt = ['app_id', 'app_secret'];
}
public function authoriseDropbox(Request $request)
{
$this->authorizeAccessToAdminPanel('admin:manage-storage');
if (!$request->has('state') && !$request->has('code'))
{
// TODO flash an error
return redirect('storages.index');
}
try
{
$storageID = decrypt($request->get('state'));
$storage = Storage::where('id', intval($storageID))->first();
if (is_null($storage))
{
// TODO flash an error
return redirect('storages.index');
}
if (is_null($storage->externalService))
{
// TODO flash an error
return redirect('storages.index');
}
switch ($storage->externalService->service_type)
{
case ExternalService::DROPBOX:
$dropbox = new DropboxService();
$dropbox->handleAuthenticationResponse($request, $storage);
// TODO flash a success message
return redirect(route('storage.index'));
default:
// TODO flash an error
return redirect('storages.index');
}
}
catch (\Exception $ex)
{
// TODO flash an error
return redirect('storages.index');
}
}
/**
* Show the form for creating a new resource.
*
@ -46,6 +95,7 @@ class ServiceController extends Controller
$this->authorizeAccessToAdminPanel('admin:manage-services');
return Theme::render('admin.create_service', [
'callbackUrls' => $this->callbackList(),
'service' => new ExternalService(),
'serviceTypes' => $this->serviceTypeList()
]);
@ -136,6 +186,7 @@ class ServiceController extends Controller
}
return Theme::render('admin.edit_service', [
'callbackUrls' => $this->callbackList(),
'service' => $service,
'serviceTypes' => $this->serviceTypeList()
]);
@ -222,6 +273,15 @@ class ServiceController extends Controller
return redirect(route('services.index'));
}
private function callbackList()
{
$dropboxService = new DropboxService();
return [
ExternalService::DROPBOX => $dropboxService->callbackUrl()
];
}
private function isServiceInUse(ExternalService $service)
{
// TODO check if the service is in use anywhere else and prevent it being deleted if so

View File

@ -28,7 +28,7 @@ class StorageController extends Controller
$this->encryptedFields = ['password', 'access_key', 'secret_key', 'access_token'];
}
public function authoriseService($id)
public function authoriseService(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-storage');
@ -38,47 +38,35 @@ class StorageController extends Controller
App::abort(404);
}
if (is_null($storage->externalService))
$externalServiceType = $this->getExternalServiceType($storage);
if (is_null($externalServiceType))
{
App::abort(400, 'Storage does not support an external service');
$request->session()->flash('error', trans('admin.storage_no_external_service_support'));
return redirect(route('storages.index'));
}
switch ($storage->externalService->service_type)
$serviceTypeName = trans(sprintf('services.%s', $externalServiceType));
$viewData = [
'service' => $storage->externalService,
'serviceName' => $serviceTypeName,
'storage' => $storage
];
switch ($externalServiceType)
{
case ExternalService::DROPBOX:
$dropbox = new DropboxService();
return redirect($dropbox->authoriseUrl($storage));
$viewData['authoriseUrl'] = $dropbox->authoriseUrl($storage);
$viewData['callbackUrl'] = $dropbox->callbackUrl();
break;
default:
App::abort(400, 'External service does not support authorisation');
}
}
public function completeServiceAuthorisation(Request $request, $id)
{
$this->authorizeAccessToAdminPanel('admin:manage-storage');
$storage = Storage::where('id', intval($id))->first();
if (is_null($storage))
{
App::abort(404);
$request->session()->flash('error', trans('admin.storage_external_service_no_authorisation', ['service_name' => $serviceTypeName]));
return redirect(route('storages.index'));
}
if (is_null($storage->externalService))
{
App::abort(400, 'Storage does not support an external service');
}
switch ($storage->externalService->service_type)
{
case ExternalService::DROPBOX:
$dropbox = new DropboxService();
$dropbox->handleAuthenticationResponse($request, $storage);
return redirect(route('storage.index'));
default:
App::abort(400, 'External service does not support authorisation');
}
return Theme::render('admin.authorise_external_service', $viewData);
}
/**
@ -175,6 +163,17 @@ class StorageController extends Controller
$this->unsetIsDefaultFromOthers($storage);
}
$externalServiceType = $this->getExternalServiceType($storage);
if (!is_null($externalServiceType))
{
switch ($externalServiceType)
{
case ExternalService::DROPBOX:
return redirect(route('storage.authoriseService', ['storage' => $storage->id]));
}
}
return redirect(route('storage.index'));
}
@ -351,6 +350,16 @@ class StorageController extends Controller
return redirect(route('storage.index'));
}
private function getExternalServiceType(Storage $storage)
{
if (!is_null($storage->externalService))
{
return $storage->externalService->service_type;
}
return null;
}
private function setIsDefaultForFirstStorage()
{
$count = Storage::where('is_default', true)->count();

View File

@ -27,16 +27,22 @@ class DropboxService
public function authoriseUrl(Storage $storage)
{
$service = $storage->externalService;
$redirectUrl = route('storage.completeServiceAuthorisation', ['storage' => $storage->id]);
$redirectUrl = $this->callbackUrl();
return sprintf(
'%s?client_id=%s&response_type=code&redirect_uri=%s',
'%s?client_id=%s&response_type=code&redirect_uri=%s&state=%s',
$this->config['authorise_url'],
urlencode(decrypt($service->app_id)),
urlencode($redirectUrl)
urlencode($redirectUrl),
urlencode(encrypt($storage->id))
);
}
public function callbackUrl()
{
return route('services.authoriseDropbox');
}
public function downloadFile($pathOnStorage)
{
$dropboxArgs = ['path' => $pathOnStorage];
@ -100,7 +106,7 @@ class DropboxService
{
$service = $storage->externalService;
$credentials = sprintf('%s:%s', decrypt($service->app_id), decrypt($service->app_secret));
$redirectUrl = route('storage.completeServiceAuthorisation', ['storage' => $storage->id]);
$redirectUrl = $this->callbackUrl();
$httpHeaders = [
'Accept: application/json',

View File

@ -7,11 +7,15 @@ function ExternalServiceViewModel()
this.computed = {
hasOAuthStandardOptions()
{
// This list must be mirrored in App\ExternalService
return this.service_type === 'dropbox' ||
this.service_type === 'facebook' ||
// This logic must be mirrored in App\ExternalService
return this.service_type === 'facebook' ||
this.service_type === 'google' ||
this.service_type === 'twitter';
},
isDropbox()
{
// This logic must be mirrored in App\ExternalService
return this.service_type === 'dropbox';
}
}
}

View File

@ -63,6 +63,10 @@ return [
'approve_comment_confirm' => 'Are you sure you want to approve this comment by ":author_name"?',
'approve_comments' => 'Approve :number comments',
'approve_comments_confirm' => 'Are you sure you want to approve these :number comments?',
'authorise_service_authorise' => 'Authorise',
'authorise_service_authorise_intro' => 'Click the Authorise button to login to :name.',
'authorise_service_intro' => 'Blue Twilight needs authorisation to access your :name account.',
'authorise_service_title' => 'Authorise access to :name',
'bulk_comments_approved' => ':number comment was approved successfully.|:number comments were approved successfully.',
'bulk_comments_deleted' => ':number comment was deleted successfully.|:number comments were deleted successfully.',
'bulk_photos_changed' => ':number photo was updated successfully.|:number photos were updated successfully.',
@ -269,6 +273,7 @@ return [
'visible_action' => 'Only those visible'
],
'select_none_action' => 'Clear selection',
'service_callback_intro' => 'Please ensure you add the below URL to your :name app as a "trusted", "callback" or "redirect" URL.',
'service_deletion_failed' => 'An error occurred while removing the :name service: :error_message',
'service_deletion_successful' => 'The :name service was removed successfully.',
'services_title' => 'External services',
@ -344,6 +349,8 @@ return [
'storage_authorise_external_service_refresh_authentication' => 'Refresh authentication',
'storage_authorise_external_service_required' => 'Authorisation required',
'storage_backblaze_access_key_id_help' => 'To use your account\'s master key, enter your account ID here.',
'storage_external_service_no_authorisation' => ':service_name does not support authentication.',
'storage_no_external_service_support' => 'This storage driver does not support an external service.',
'storage_s3_signed_urls_help' => 'When enabled, Blue Twilight will upload your photos with a private ACL and will use signed URLs to display the photos to your visitors.',
'storage_s3_signed_urls_tooltip' => 'This location is set to use private images with signed URLs.',
'storage_title' => 'Storage Locations',

View File

@ -6,6 +6,7 @@ return [
'admin' => 'Admin',
'albums' => 'Albums',
'approve_comment' => 'Approve comment',
'authorise_service' => 'Authorise service',
'comments' => 'Comments',
'create_album' => 'Create album',
'create_group' => 'Create group',

View File

@ -0,0 +1,27 @@
@extends(Theme::viewName('layout'))
@section('title', trans('admin.authorise_service_title', ['name' => $serviceName]))
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('home') }}"><i class="fa fa-fw fa-home"></i></a></li>
<li class="breadcrumb-item"><a href="{{ route('admin') }}">@lang('navigation.breadcrumb.admin')</a></li>
<li class="breadcrumb-item"><a href="{{ route('storage.index') }}">@lang('navigation.breadcrumb.storage')</a></li>
<li class="breadcrumb-item active">@lang('navigation.breadcrumb.authorise_service')</li>
@endsection
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 ml-md-auto mr-md-auto">
<div class="card bg-info">
<div class="card-header text-white">@yield('title')</div>
<div class="card-body bg-light">
<p>@lang('admin.authorise_service_intro', ['name' => $serviceName])</p>
<p class="text-success">@lang('admin.authorise_service_authorise_intro', ['name' => $serviceName])</p>
<p class="text-right mb-0"><a href="{{ $authoriseUrl }}" class="btn btn-primary">@lang('admin.authorise_service_authorise')</a></p>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -50,6 +50,9 @@
<div v-if="hasOAuthStandardOptions">
@include(Theme::viewName('partials.admin_services_oauth_options'))
</div>
<div v-elseif="isDropbox">
@include(Theme::viewName('partials.admin_services_dropbox_options'))
</div>
<div class="text-right">
<a href="{{ route('services.index') }}" class="btn btn-link">@lang('forms.cancel_action')</a>

View File

@ -50,6 +50,8 @@
@if ($service->hasOAuthStandardOptions())
@include(Theme::viewName('partials.admin_services_oauth_options'))
@elseif ($service->isDropbox())
@include(Theme::viewName('partials.admin_services_dropbox_options'))
@endif
<div class="text-right" style="margin-top: 20px;">

View File

@ -0,0 +1,31 @@
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="access-key">@lang('forms.service_app_id_label')</label>
<input type="text" class="form-control{{ $errors->has('app_id') ? ' is-invalid' : '' }}" id="app-id" name="app_id" value="{{ old('app_id', $service->app_id) }}">
@if ($errors->has('app_id'))
<div class="invalid-feedback">
<strong>{{ $errors->first('app_id') }}</strong>
</div>
@endif
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-control-label" for="secret-key">@lang('forms.service_app_secret_label')</label>
<input type="text" class="form-control{{ $errors->has('app_secret') ? ' is-invalid' : '' }}" id="app-secret" name="app_secret" value="{{ old('app_secret', $service->app_secret) }}">
@if ($errors->has('app_secret'))
<div class="invalid-feedback">
<strong>{{ $errors->first('app_secret') }}</strong>
</div>
@endif
</div>
</div>
</div>
<div class="alert alert-info">
<p>@lang('admin.service_callback_intro', ['name' => trans(sprintf('services.%s', \App\ExternalService::DROPBOX))])</p>
<p class="mb-0"><b>{{ $callbackUrls[\App\ExternalService::DROPBOX] }}</b></p>
</div>

View File

@ -56,7 +56,6 @@ Route::group(['prefix' => 'admin'], function () {
// Storage management
Route::get('storage/{storage}/authorise-service', 'Admin\StorageController@authoriseService')->name('storage.authoriseService');
Route::get('storage/{storage}/complete-service-authorisation', 'Admin\StorageController@completeServiceAuthorisation')->name('storage.completeServiceAuthorisation');
Route::get('storage/{storage}/delete', 'Admin\StorageController@delete')->name('storage.delete');
Route::resource('storage', 'Admin\StorageController');
@ -83,6 +82,7 @@ Route::group(['prefix' => 'admin'], function () {
Route::resource('comments', 'Admin\PhotoCommentController');
// Services management
Route::get('services/authorise-dropbox', 'Admin\ServiceController@authoriseDropbox')->name('services.authoriseDropbox');
Route::get('services/{service}/delete', 'Admin\ServiceController@delete')->name('services.delete');
Route::resource('services', 'Admin\ServiceController');
});