Pull #106 and #148 Dropbox and external services #149

Merged
aheathershaw merged 9 commits from feature/106-dropbox-storage into master 2020-04-22 06:58:15 +01:00
12 changed files with 197 additions and 42 deletions
Showing only changes of commit f17a84f746 - Show all commits

View File

@ -30,12 +30,17 @@ class ExternalService extends Model
public function hasOAuthStandardOptions() 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, [ return in_array($this->service_type, [
self::DROPBOX,
self::FACEBOOK, self::FACEBOOK,
self::GOOGLE, self::GOOGLE,
self::TWITTER 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\Facade\UserConfig;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\StoreServiceRequest; use App\Http\Requests\StoreServiceRequest;
use App\Services\DropboxService;
use App\Storage;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
@ -36,6 +38,53 @@ class ServiceController extends Controller
$this->fieldsToEncrypt = ['app_id', 'app_secret']; $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. * Show the form for creating a new resource.
* *
@ -46,6 +95,7 @@ class ServiceController extends Controller
$this->authorizeAccessToAdminPanel('admin:manage-services'); $this->authorizeAccessToAdminPanel('admin:manage-services');
return Theme::render('admin.create_service', [ return Theme::render('admin.create_service', [
'callbackUrls' => $this->callbackList(),
'service' => new ExternalService(), 'service' => new ExternalService(),
'serviceTypes' => $this->serviceTypeList() 'serviceTypes' => $this->serviceTypeList()
]); ]);
@ -136,6 +186,7 @@ class ServiceController extends Controller
} }
return Theme::render('admin.edit_service', [ return Theme::render('admin.edit_service', [
'callbackUrls' => $this->callbackList(),
'service' => $service, 'service' => $service,
'serviceTypes' => $this->serviceTypeList() 'serviceTypes' => $this->serviceTypeList()
]); ]);
@ -222,6 +273,15 @@ class ServiceController extends Controller
return redirect(route('services.index')); return redirect(route('services.index'));
} }
private function callbackList()
{
$dropboxService = new DropboxService();
return [
ExternalService::DROPBOX => $dropboxService->callbackUrl()
];
}
private function isServiceInUse(ExternalService $service) private function isServiceInUse(ExternalService $service)
{ {
// TODO check if the service is in use anywhere else and prevent it being deleted if so // 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']; $this->encryptedFields = ['password', 'access_key', 'secret_key', 'access_token'];
} }
public function authoriseService($id) public function authoriseService(Request $request, $id)
{ {
$this->authorizeAccessToAdminPanel('admin:manage-storage'); $this->authorizeAccessToAdminPanel('admin:manage-storage');
@ -38,47 +38,35 @@ class StorageController extends Controller
App::abort(404); 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: case ExternalService::DROPBOX:
$dropbox = new DropboxService(); $dropbox = new DropboxService();
return redirect($dropbox->authoriseUrl($storage)); $viewData['authoriseUrl'] = $dropbox->authoriseUrl($storage);
$viewData['callbackUrl'] = $dropbox->callbackUrl();
break;
default: default:
App::abort(400, 'External service does not support authorisation'); $request->session()->flash('error', trans('admin.storage_external_service_no_authorisation', ['service_name' => $serviceTypeName]));
} return redirect(route('storages.index'));
} }
public function completeServiceAuthorisation(Request $request, $id) return Theme::render('admin.authorise_external_service', $viewData);
{
$this->authorizeAccessToAdminPanel('admin:manage-storage');
$storage = Storage::where('id', intval($id))->first();
if (is_null($storage))
{
App::abort(404);
}
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');
}
} }
/** /**
@ -175,6 +163,17 @@ class StorageController extends Controller
$this->unsetIsDefaultFromOthers($storage); $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')); return redirect(route('storage.index'));
} }
@ -351,6 +350,16 @@ class StorageController extends Controller
return redirect(route('storage.index')); 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() private function setIsDefaultForFirstStorage()
{ {
$count = Storage::where('is_default', true)->count(); $count = Storage::where('is_default', true)->count();

View File

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

View File

@ -7,11 +7,15 @@ function ExternalServiceViewModel()
this.computed = { this.computed = {
hasOAuthStandardOptions() hasOAuthStandardOptions()
{ {
// This list must be mirrored in App\ExternalService // This logic must be mirrored in App\ExternalService
return this.service_type === 'dropbox' || return this.service_type === 'facebook' ||
this.service_type === 'facebook' ||
this.service_type === 'google' || this.service_type === 'google' ||
this.service_type === 'twitter'; 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_comment_confirm' => 'Are you sure you want to approve this comment by ":author_name"?',
'approve_comments' => 'Approve :number comments', 'approve_comments' => 'Approve :number comments',
'approve_comments_confirm' => 'Are you sure you want to approve these :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_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_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.', 'bulk_photos_changed' => ':number photo was updated successfully.|:number photos were updated successfully.',
@ -269,6 +273,7 @@ return [
'visible_action' => 'Only those visible' 'visible_action' => 'Only those visible'
], ],
'select_none_action' => 'Clear selection', '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_failed' => 'An error occurred while removing the :name service: :error_message',
'service_deletion_successful' => 'The :name service was removed successfully.', 'service_deletion_successful' => 'The :name service was removed successfully.',
'services_title' => 'External services', 'services_title' => 'External services',
@ -344,6 +349,8 @@ return [
'storage_authorise_external_service_refresh_authentication' => 'Refresh authentication', 'storage_authorise_external_service_refresh_authentication' => 'Refresh authentication',
'storage_authorise_external_service_required' => 'Authorisation required', '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_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_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_s3_signed_urls_tooltip' => 'This location is set to use private images with signed URLs.',
'storage_title' => 'Storage Locations', 'storage_title' => 'Storage Locations',

View File

@ -6,6 +6,7 @@ return [
'admin' => 'Admin', 'admin' => 'Admin',
'albums' => 'Albums', 'albums' => 'Albums',
'approve_comment' => 'Approve comment', 'approve_comment' => 'Approve comment',
'authorise_service' => 'Authorise service',
'comments' => 'Comments', 'comments' => 'Comments',
'create_album' => 'Create album', 'create_album' => 'Create album',
'create_group' => 'Create group', '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"> <div v-if="hasOAuthStandardOptions">
@include(Theme::viewName('partials.admin_services_oauth_options')) @include(Theme::viewName('partials.admin_services_oauth_options'))
</div> </div>
<div v-elseif="isDropbox">
@include(Theme::viewName('partials.admin_services_dropbox_options'))
</div>
<div class="text-right"> <div class="text-right">
<a href="{{ route('services.index') }}" class="btn btn-link">@lang('forms.cancel_action')</a> <a href="{{ route('services.index') }}" class="btn btn-link">@lang('forms.cancel_action')</a>

View File

@ -50,6 +50,8 @@
@if ($service->hasOAuthStandardOptions()) @if ($service->hasOAuthStandardOptions())
@include(Theme::viewName('partials.admin_services_oauth_options')) @include(Theme::viewName('partials.admin_services_oauth_options'))
@elseif ($service->isDropbox())
@include(Theme::viewName('partials.admin_services_dropbox_options'))
@endif @endif
<div class="text-right" style="margin-top: 20px;"> <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 // Storage management
Route::get('storage/{storage}/authorise-service', 'Admin\StorageController@authoriseService')->name('storage.authoriseService'); 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::get('storage/{storage}/delete', 'Admin\StorageController@delete')->name('storage.delete');
Route::resource('storage', 'Admin\StorageController'); Route::resource('storage', 'Admin\StorageController');
@ -83,6 +82,7 @@ Route::group(['prefix' => 'admin'], function () {
Route::resource('comments', 'Admin\PhotoCommentController'); Route::resource('comments', 'Admin\PhotoCommentController');
// Services management // 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::get('services/{service}/delete', 'Admin\ServiceController@delete')->name('services.delete');
Route::resource('services', 'Admin\ServiceController'); Route::resource('services', 'Admin\ServiceController');
}); });