diff --git a/app/ExternalService.php b/app/ExternalService.php index 05bd0b0..8f50d76 100644 --- a/app/ExternalService.php +++ b/app/ExternalService.php @@ -6,18 +6,36 @@ use Illuminate\Database\Eloquent\Model; class ExternalService extends Model { - public const DROPBOX = 'Dropbox'; - public const FACEBOOK = 'Facebook'; - public const GOOGLE = 'Google'; - public const TWITTER = 'Twitter'; + public const DROPBOX = 'dropbox'; + public const FACEBOOK = 'facebook'; + public const GOOGLE = 'google'; + public const TWITTER = 'twitter'; /** - * Gets the details for the given service type. + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = ['name', 'service_type']; + + /** + * Gets all possible service configurations for the given service type. * @param $serviceType - * @return ExternalService + * @return ExternalService[] */ public static function getForService($serviceType) { - return ExternalService::where('service_type', $serviceType)->first(); + return ExternalService::where('service_type', $serviceType)->get(); + } + + public function hasOAuthStandardOptions() + { + // This list must be mirrored in external_services.js + return in_array($this->service_type, [ + self::DROPBOX, + self::FACEBOOK, + self::GOOGLE, + self::TWITTER + ]); } } \ No newline at end of file diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index acc585e..c71f779 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -2,9 +2,239 @@ namespace App\Http\Controllers\Admin; +use App\ExternalService; +use App\Facade\Theme; +use App\Facade\UserConfig; use App\Http\Controllers\Controller; +use App\Http\Requests\StoreServiceRequest; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\View; class ServiceController extends Controller { + /** + * List of fields that must be encrypted before being saved. + * + * @var string[] + */ + private $fieldsToEncrypt; + /** + * List of fields that depend on the service_type being configured. + * + * @var string[] + */ + private $serviceTypeDependentFields; + + public function __construct() + { + $this->middleware('auth'); + View::share('is_admin', true); + + $this->serviceTypeDependentFields = ['app_id', 'app_secret']; + $this->fieldsToEncrypt = ['app_id', 'app_secret']; + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + */ + public function create() + { + $this->authorizeAccessToAdminPanel('admin:manage-services'); + + return Theme::render('admin.create_service', [ + 'service' => new ExternalService(), + 'serviceTypes' => $this->serviceTypeList() + ]); + } + + public function delete(Request $request, $id) + { + $this->authorizeAccessToAdminPanel('admin:manage-users'); + + $service = ExternalService::where('id', intval($id))->first(); + if (is_null($service)) + { + App::abort(404); + } + + if ($this->isServiceInUse($service)) + { + $request->session()->flash('warning', trans('admin.cannot_delete_service_in_use')); + return redirect(route('services.index')); + } + + return Theme::render('admin.delete_service', ['service' => $service]); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function destroy(Request $request, $id) + { + $this->authorizeAccessToAdminPanel('admin:manage-services'); + + $service = ExternalService::where('id', intval($id))->first(); + if (is_null($service)) + { + App::abort(404); + } + + if ($this->isServiceInUse($service)) + { + $request->session()->flash('warning', trans('admin.cannot_delete_service_in_use')); + return redirect(route('services.index')); + } + + try + { + $service->delete(); + $request->session()->flash('success', trans('admin.service_deletion_successful', [ + 'name' => $service->name + ])); + } + catch (\Exception $ex) + { + $request->session()->flash('error', trans('admin.service_deletion_failed', [ + 'error_message' => $ex->getMessage(), + 'name' => $service->name + ])); + } + + return redirect(route('services.index')); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + */ + public function edit(Request $request, $id) + { + $this->authorizeAccessToAdminPanel('admin:manage-services'); + + $service = ExternalService::where('id', intval($id))->first(); + if (is_null($service)) + { + App::abort(404); + } + + // Decrypt the fields that are stored as encrypted in the DB + foreach ($this->fieldsToEncrypt as $field) + { + if (!empty($service->$field)) + { + $service->$field = decrypt($service->$field); + } + } + + return Theme::render('admin.edit_service', [ + 'service' => $service, + 'serviceTypes' => $this->serviceTypeList() + ]); + } + + /** + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function index(Request $request) + { + $this->authorizeAccessToAdminPanel('admin:manage-services'); + + $services = ExternalService::orderBy('name') + ->paginate(UserConfig::get('items_per_page')); + + return Theme::render('admin.list_services', [ + 'error' => $request->session()->get('error'), + 'services' => $services, + 'success' => $request->session()->get('success'), + 'warning' => $request->session()->get('warning') + ]); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(StoreServiceRequest $request) + { + $this->authorizeAccessToAdminPanel('admin:manage-services'); + + $service = new ExternalService($request->only(['name', 'service_type'])); + + foreach ($this->serviceTypeDependentFields as $field) + { + if ($request->has($field)) + { + $service->$field = in_array($field, $this->fieldsToEncrypt) + ? encrypt($request->get($field)) + : $request->get($field); + } + } + + $service->save(); + + return redirect(route('services.index')); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param int $id + * @return \Illuminate\Http\Response + */ + public function update(StoreServiceRequest $request, $id) + { + $this->authorizeAccessToAdminPanel('admin:manage-services'); + + $service = ExternalService::where('id', intval($id))->first(); + if (is_null($service)) + { + App::abort(404); + } + + $service->fill($request->only(['name', 'service_type'])); + + foreach ($this->serviceTypeDependentFields as $field) + { + if ($request->has($field)) + { + $service->$field = in_array($field, $this->fieldsToEncrypt) + ? encrypt($request->get($field)) + : $request->get($field); + } + } + + $service->save(); + + return redirect(route('services.index')); + } + + private function isServiceInUse(ExternalService $service) + { + // TODO check if the service is in use anywhere else and prevent it being deleted if so + return false; + } + + private function serviceTypeList() + { + return [ + ExternalService::DROPBOX => trans(sprintf('services.%s', ExternalService::DROPBOX)), + ExternalService::FACEBOOK => trans(sprintf('services.%s', ExternalService::FACEBOOK)), + ExternalService::GOOGLE => trans(sprintf('services.%s', ExternalService::GOOGLE)), + ExternalService::TWITTER => trans(sprintf('services.%s', ExternalService::TWITTER)) + ]; + } } \ No newline at end of file diff --git a/app/Http/Controllers/Admin/StorageController.php b/app/Http/Controllers/Admin/StorageController.php index 4678fa7..3a0b7dc 100644 --- a/app/Http/Controllers/Admin/StorageController.php +++ b/app/Http/Controllers/Admin/StorageController.php @@ -2,10 +2,12 @@ namespace App\Http\Controllers\Admin; +use App\ExternalService; use App\Facade\Theme; use App\Facade\UserConfig; use App\Http\Controllers\Controller; use App\Http\Requests; +use App\Services\DropboxService; use App\Storage; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; @@ -26,6 +28,59 @@ class StorageController extends Controller $this->encryptedFields = ['password', 'access_key', 'secret_key', 'access_token']; } + public function authoriseService($id) + { + $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(); + return redirect($dropbox->authoriseUrl($storage)); + + 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); + } + + 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'); + } + } + /** * Display a listing of the resource. * @@ -60,6 +115,7 @@ class StorageController extends Controller return Theme::render('admin.create_storage', [ 'album_sources' => UserConfig::albumSources(), + 'dropbox_services' => ExternalService::getForService(ExternalService::DROPBOX), 'filesystem_default_location' => $filesystemDefaultLocation, 'info' => $request->session()->get('info'), 'storage' => $storage @@ -92,7 +148,7 @@ class StorageController extends Controller 'access_key', 'secret_key', 'b2_bucket_type', - 'access_token' + 'external_service_id' ])); $storage->is_active = true; $storage->is_default = (strtolower($request->get('is_default')) == 'on'); @@ -191,7 +247,10 @@ class StorageController extends Controller } } - return Theme::render('admin.edit_storage', ['storage' => $storage]); + return Theme::render('admin.edit_storage', [ + 'dropbox_services' => ExternalService::getForService(ExternalService::DROPBOX), + 'storage' => $storage + ]); } /** @@ -224,8 +283,7 @@ class StorageController extends Controller 'access_key', 'secret_key', 'b2_bucket_type', - 'access_token', - 's3_signed_urls' + 'external_service_id' ])); $storage->is_active = (strtolower($request->get('is_active')) == 'on'); $storage->is_default = (strtolower($request->get('is_default')) == 'on'); diff --git a/app/Http/Requests/StoreServiceRequest.php b/app/Http/Requests/StoreServiceRequest.php new file mode 100644 index 0000000..5dc47de --- /dev/null +++ b/app/Http/Requests/StoreServiceRequest.php @@ -0,0 +1,74 @@ +method()) + { + case 'POST': + $result = [ + 'name' => 'required|unique:external_services|max:255', + 'service_type' => 'required|max:255', + ]; + + switch ($this->get('service_type')) + { + case ExternalService::DROPBOX: + case ExternalService::FACEBOOK: + case ExternalService::GOOGLE: + case ExternalService::TWITTER: + // Standard OAuth services + $result['app_id'] = 'sometimes|required'; + $result['app_secret'] = 'sometimes|required'; + break; + } + break; + + case 'PATCH': + case 'PUT': + $serviceId = intval($this->segment(3)); + $service = ExternalService::find($serviceId); + $result = [ + 'name' => 'required|max:255|unique:external_services,name,' . $serviceId + ]; + + switch ($service->service_type) + { + case ExternalService::DROPBOX: + case ExternalService::FACEBOOK: + case ExternalService::GOOGLE: + case ExternalService::TWITTER: + // Standard OAuth services + $result['app_id'] = 'sometimes|required'; + $result['app_secret'] = 'sometimes|required'; + break; + } + break; + } + + return $result; + } +} diff --git a/app/Http/Requests/StoreStorageRequest.php b/app/Http/Requests/StoreStorageRequest.php index 5d3e4d8..acee6de 100644 --- a/app/Http/Requests/StoreStorageRequest.php +++ b/app/Http/Requests/StoreStorageRequest.php @@ -73,9 +73,7 @@ class StoreStorageRequest extends FormRequest break; case 'DropboxSource': - $result['access_key'] = 'sometimes|required'; - $result['secret_key'] = 'sometimes|required'; - $result['access_token'] = 'sometimes|required'; + $result['external_service_id'] = 'sometimes|required'; break; } break; @@ -123,9 +121,7 @@ class StoreStorageRequest extends FormRequest break; case 'DropboxSource': - $result['access_key'] = 'sometimes|required'; - $result['secret_key'] = 'sometimes|required'; - $result['access_token'] = 'sometimes|required'; + $result['external_service_id'] = 'sometimes|required'; break; } break; diff --git a/app/Services/DropboxService.php b/app/Services/DropboxService.php index 5fca0fc..2a1a885 100644 --- a/app/Services/DropboxService.php +++ b/app/Services/DropboxService.php @@ -2,6 +2,8 @@ namespace App\Services; +use App\Storage; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; class DropboxService @@ -22,6 +24,19 @@ class DropboxService $this->config = config('services.dropbox'); } + public function authoriseUrl(Storage $storage) + { + $service = $storage->externalService; + $redirectUrl = route('storage.completeServiceAuthorisation', ['storage' => $storage->id]); + + return sprintf( + '%s?client_id=%s&response_type=code&redirect_uri=%s', + $this->config['authorise_url'], + urlencode(decrypt($service->app_id)), + urlencode($redirectUrl) + ); + } + public function downloadFile($pathOnStorage) { $dropboxArgs = ['path' => $pathOnStorage]; @@ -41,6 +56,16 @@ class DropboxService ); } + public function handleAuthenticationResponse(Request $request, Storage $storage) + { + $authorisationCode = $request->query('code'); + + $storage->access_token = encrypt($this->convertAuthorisationCodeToToken($authorisationCode, $storage)); + $storage->save(); + + return true; + } + /** * @param string $accessToken */ @@ -71,6 +96,40 @@ class DropboxService ); } + private function convertAuthorisationCodeToToken($authorisationCode, Storage $storage) + { + $service = $storage->externalService; + $credentials = sprintf('%s:%s', decrypt($service->app_id), decrypt($service->app_secret)); + $redirectUrl = route('storage.completeServiceAuthorisation', ['storage' => $storage->id]); + + $httpHeaders = [ + 'Accept: application/json', + sprintf('Authorization: Basic %s', base64_encode($credentials)) + ]; + + $ch = curl_init($this->config['token_url']); + curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, [ + 'code' => $authorisationCode, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $redirectUrl + ]); + + $response = json_decode(curl_exec($ch)); + if (is_null($response) || $response === false) + { + throw new \Exception('Unable to read the response from Dropbox'); + } + else if (isset($response->error_description)) + { + throw new \Exception(sprintf('Error from Dropbox: %s', $response->error_description)); + } + + return $response->access_token; + } + private function getBasicHttpClient($url, $method = 'GET', array $httpHeaders = []) { $httpHeaders = array_merge( diff --git a/app/Storage.php b/app/Storage.php index beecf04..fa435f4 100644 --- a/app/Storage.php +++ b/app/Storage.php @@ -32,11 +32,16 @@ class Storage extends Model 'access_key', 'secret_key', 'b2_bucket_type', - 'access_token' + 'external_service_id' ]; public function albums() { return $this->hasMany(Album::class); } + + public function externalService() + { + return $this->belongsTo(ExternalService::class); + } } diff --git a/composer.json b/composer.json index 5ec607f..6ecc818 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "pandy06269/blue-twilight", + "name": "aheathershaw/blue-twilight", "description": "Blue Twilight - self-hosted photo gallery software.", "keywords": ["blue", "twilight", "photo", "photograph", "portfolio", "gallery", "self-hosted"], "license": "MIT", diff --git a/config/services.php b/config/services.php index a67c763..10e07af 100644 --- a/config/services.php +++ b/config/services.php @@ -20,7 +20,9 @@ return [ ], 'dropbox' => [ + 'authorise_url' => 'https://www.dropbox.com/oauth2/authorize', 'download_url' => 'https://content.dropboxapi.com/2/files/download', + 'token_url' => 'https://api.dropbox.com/oauth2/token', 'upload_url' => 'https://content.dropboxapi.com/2/files/upload' ], diff --git a/database/migrations/2020_04_19_085433_create_external_services_table.php b/database/migrations/2020_04_19_085433_create_external_services_table.php index 9502aba..c9e6915 100644 --- a/database/migrations/2020_04_19_085433_create_external_services_table.php +++ b/database/migrations/2020_04_19_085433_create_external_services_table.php @@ -16,6 +16,7 @@ class CreateExternalServicesTable extends Migration Schema::create('external_services', function (Blueprint $table) { $table->increments('id'); $table->string('service_type', 50); + $table->string('name'); $table->text('app_id')->nullable(); $table->text('app_secret')->nullable(); $table->timestamps(); diff --git a/database/migrations/2020_04_19_181020_add_service_to_storages_table.php b/database/migrations/2020_04_19_181020_add_service_to_storages_table.php new file mode 100644 index 0000000..5d36dfb --- /dev/null +++ b/database/migrations/2020_04_19_181020_add_service_to_storages_table.php @@ -0,0 +1,37 @@ +unsignedInteger('external_service_id')->nullable(); + + $table->foreign('external_service_id') + ->references('id') + ->on('external_services'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('storages', function (Blueprint $table) { + $table->dropForeign('storages_external_service_id_foreign'); + $table->dropColumn('external_service_id'); + }); + } +} diff --git a/resources/js/external_services.js b/resources/js/external_services.js new file mode 100644 index 0000000..5bbf5aa --- /dev/null +++ b/resources/js/external_services.js @@ -0,0 +1,17 @@ +function ExternalServiceViewModel() +{ + this.el = '#external-service-options'; + this.data = { + service_type: '' + }; + this.computed = { + hasOAuthStandardOptions() + { + // This list must be mirrored in App\ExternalService + return this.service_type === 'dropbox' || + this.service_type === 'facebook' || + this.service_type === 'google' || + this.service_type === 'twitter'; + } + } +} \ No newline at end of file diff --git a/resources/lang/en/admin.php b/resources/lang/en/admin.php index 1a8561a..6cb3b37 100644 --- a/resources/lang/en/admin.php +++ b/resources/lang/en/admin.php @@ -68,6 +68,7 @@ return [ 'bulk_photos_changed' => ':number photo was updated successfully.|:number photos were updated successfully.', 'bulk_photos_changed_queued' => 'Your requested change has been queued. :number photo will be updated shortly.|Your requested change has been queued. :number photos will be updated shortly.', 'cannot_delete_own_user_account' => 'It is not possible to delete your own user account. Please ask another administrator to delete it for you.', + 'cannot_delete_service_in_use' => 'This service is still in use and cannot be deleted. Please remove any references to it in your configuration and try again.', 'cannot_remove_own_admin' => 'You cannot remove your own administrator permissions. Please ask another administrator to remove the administrator permissions for you.', 'change_album_message' => 'Please select the album to move the photo(s) to:', 'change_album_title' => 'Move photo(s) to another album', @@ -91,6 +92,9 @@ return [ 'create_redirect_heading' => 'Add a Redirect', 'create_redirect_success_message' => 'The redirect was added successfully.', 'create_redirect_text' => 'Enter the source address you would like to redirect and click the Create button to add a new redirect to this album.', + 'create_service' => 'Create service', + 'create_service_intro' => 'You can use the form below to create a service. Your service provider (e.g. Facebook, Twitter, Dropbox) will provide you with the details required.', + 'create_service_title' => 'Create a service', 'create_storage' => 'Create storage location', 'create_storage_intro' => 'Complete the form below to create a new storage location to hold your photos. You can then select this storage location when you create an album.', 'create_user' => 'Create user', @@ -129,6 +133,9 @@ return [ 'delete_redirect_confirm_message' => 'Are you sure you want to remove this redirect?', 'delete_redirect_confirm_title' => 'Delete Album Redirect', 'delete_redirect_success_message' => 'The redirect was deleted successfully.', + 'delete_service' => 'Delete service: :name', + 'delete_service_confirm' => 'Are you sure you want to permanently remove the :name service?', + 'delete_service_warning' => 'This is a permanent action that cannot be reversed!', 'delete_storage' => 'Delete storage location: :name', 'delete_storage_confirm' => 'Are you sure you want to permanently remove this storage location?', 'delete_storage_existing_albums' => 'At least one album is still using the storage location. Please delete all albums before removing the storage location.', @@ -145,6 +152,8 @@ return [ 'edit_album_intro2' => 'Complete the form below to edit the properties of the album: :album_name.', 'edit_group_intro' => 'You can use the form below to edit the above group. Changes take effect immediately.', 'edit_group_title' => 'Edit group: :group_name', + 'edit_service_intro' => 'You can use the form below to edit the above services. Changes take effect the next time Blue Twilight accesses the service.', + 'edit_service_title' => 'Edit service: :name', 'edit_storage' => 'Edit storage location: :storage_name', 'edit_storage_intro' => 'Use the form below to update the details of the :storage_name storage location.', 'edit_user_intro' => 'You can use the form below to edit the above user account. Changes take effect immediately.', @@ -177,6 +186,8 @@ return [ 'list_groups_title' => 'Groups', 'list_labels_intro' => 'Organise your photos differently using labels. Assign one or more labels to your photos and your visitors can view all photos with a specific tag in a single view.', 'list_labels_title' => 'Labels', + 'list_services_intro' => 'External services link your Blue Twilight application to other providers such as Facebook, Twitter and Dropbox. Configure your application\'s credentials for these services here.', + 'list_services_title' => 'Services', 'list_storages_intro' => 'Storage locations specify the physical location where your photograph files are held. This may be on your local server\'s filesystem, or on a cloud storage provider such as Rackspace or Amazon S3.', 'list_storages_title' => 'Storage Locations', 'list_users_intro' => 'User accounts allow people to login to your gallery to manage your albums. If you have disabled self-registration, you can create user accounts here to allow people to login.', @@ -212,6 +223,8 @@ return [ 'no_labels_text' => 'You have no labels yet. Use the form below to create one.', 'no_labels_title' => 'No Labels', 'no_photo_selected_message' => 'Please select at least one photo.', + 'no_services_text' => 'You have no services yet. Click the Create button to add connection details to an external service such as Facebook or Dropbox.', + 'no_services_title' => 'No External Services', 'no_storages_text' => 'You need a storage location to store your uploaded photographs.', 'no_storages_text2' => 'This can be on your server\'s local filesystem or a cloud location such as Amazon S3 or Rackspace.', 'no_storages_title' => 'No storage locations defined', @@ -256,6 +269,9 @@ return [ 'visible_action' => 'Only those visible' ], 'select_none_action' => 'Clear selection', + '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', 'settings' => [ 'albums_menu_heading' => 'Albums Navigation Menu', 'albums_menu_number_items' => 'Number of albums to display:', @@ -324,6 +340,9 @@ return [ 'users' => 'user|users', ], 'storage_auth_url_label_help' => 'Leave blank to authenticate with Amazon S3. For an S3-compatible provider, enter your provider\'s authentication URL here.', + 'storage_authorise_external_service_authorised' => 'Authorised', + '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_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.', diff --git a/resources/lang/en/forms.php b/resources/lang/en/forms.php index b514996..5ecaa43 100644 --- a/resources/lang/en/forms.php +++ b/resources/lang/en/forms.php @@ -57,8 +57,11 @@ return [ 'remove_action' => 'Remove', 'review_photo_comment_action' => 'Approve/reject comment', 'save_action' => 'Save Changes', + 'service_type_label' => 'Type of service:', 'select' => 'Select', 'select_current_text' => '(current)', + 'service_app_id_label' => 'Application ID / client ID:', + 'service_app_secret_label' => 'Application ID / client secret:', 'settings_allow_photo_comments' => 'Allow comments on photos', 'settings_allow_photo_comments_anonymous' => 'Allow anonymous users to comment on photos', 'settings_allow_photo_comments_anonymous_help' => 'With this option enabled, users can post comments without being logged in.', @@ -114,6 +117,7 @@ return [ 'storage_container_name_label' => 'Container name:', 'storage_driver_label' => 'Storage driver:', 'storage_endpoint_url_label' => 'Endpoint URL (leave blank if using Amazon):', + 'storage_external_service_label' => 'Service:', 'storage_location_label' => 'Physical location:', 'storage_s3_signed_urls' => 'Upload files privately and use signed URLs', 'storage_secret_key_label' => 'Secret key:', diff --git a/resources/lang/en/navigation.php b/resources/lang/en/navigation.php index 6e86d3b..d385045 100644 --- a/resources/lang/en/navigation.php +++ b/resources/lang/en/navigation.php @@ -9,6 +9,7 @@ return [ 'comments' => 'Comments', 'create_album' => 'Create album', 'create_group' => 'Create group', + 'create_service' => 'Create service', 'create_storage' => 'Create storage', 'create_user' => 'Create user', 'default_album_permissions' => 'Default album permissions', @@ -16,10 +17,12 @@ return [ 'delete_comment' => 'Delete comment', 'delete_group' => 'Delete group', 'delete_label' => 'Delete label', + 'delete_service' => 'Delete service', 'delete_storage' => 'Delete storage location', 'delete_user' => 'Delete user', 'edit_album' => 'Edit album', 'edit_group' => 'Edit group', + 'edit_service' => 'Edit service', 'edit_storage' => 'Edit storage location', 'edit_user' => 'Edit user', 'exif_data' => 'Exif Data', diff --git a/resources/lang/en/services.php b/resources/lang/en/services.php new file mode 100644 index 0000000..91f5821 --- /dev/null +++ b/resources/lang/en/services.php @@ -0,0 +1,7 @@ + 'Dropbox', + 'facebook' => 'Facebook', + 'google' => 'Google', + 'twitter' => 'Twitter' +]; \ No newline at end of file diff --git a/resources/views/themes/base/admin/create_service.blade.php b/resources/views/themes/base/admin/create_service.blade.php new file mode 100644 index 0000000..1a3a7cf --- /dev/null +++ b/resources/views/themes/base/admin/create_service.blade.php @@ -0,0 +1,76 @@ +@extends(Theme::viewName('layout')) +@section('title', trans('admin.create_service')) + +@section('breadcrumb') + + + + +@endsection + +@section('content') +
+
+
+

@lang('admin.create_service_title')

+

@lang('admin.create_service_intro')

+
+ +
+ {{ csrf_field() }} + +
+ + + + @if ($errors->has('service_type')) +
+ {{ $errors->first('service_type') }} +
+ @endif +
+ +
+ + + + @if ($errors->has('name')) +
+ {{ $errors->first('name') }} +
+ @endif +
+ +
+ @include(Theme::viewName('partials.admin_services_oauth_options')) +
+ +
+ @lang('forms.cancel_action') + +
+
+
+
+
+@endsection + +@push('scripts') + +@endpush \ No newline at end of file diff --git a/resources/views/themes/base/admin/delete_service.blade.php b/resources/views/themes/base/admin/delete_service.blade.php new file mode 100644 index 0000000..beffb3b --- /dev/null +++ b/resources/views/themes/base/admin/delete_service.blade.php @@ -0,0 +1,34 @@ +@extends(Theme::viewName('layout')) +@section('title', trans('admin.delete_service', ['name' => $service->name])) + +@section('breadcrumb') + + + + +@endsection + +@section('content') +
+
+
+
+
@yield('title')
+
+

@lang('admin.delete_service_confirm', ['name' => $service->name])

+

@lang('admin.delete_service_warning')

+ +
+
+ {{ csrf_field() }} + {{ method_field('DELETE') }} + @lang('forms.cancel_action') + +
+
+
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/themes/base/admin/edit_service.blade.php b/resources/views/themes/base/admin/edit_service.blade.php new file mode 100644 index 0000000..adaefe7 --- /dev/null +++ b/resources/views/themes/base/admin/edit_service.blade.php @@ -0,0 +1,63 @@ +@extends(Theme::viewName('layout')) +@section('title', trans('admin.edit_service_title', ['name' => $service->name])) + +@section('breadcrumb') + + + + +@endsection + +@section('content') +
+
+
+

@yield('title')

+

@lang('admin.edit_service_intro')

+
+ +
+ {{ csrf_field() }} + {{ method_field('PUT') }} + +
+ + + + @if ($errors->has('service_type')) +
+ {{ $errors->first('service_type') }} +
+ @endif +
+ +
+ + + + @if ($errors->has('name')) +
+ {{ $errors->first('name') }} +
+ @endif +
+ + @if ($service->hasOAuthStandardOptions()) + @include(Theme::viewName('partials.admin_services_oauth_options')) + @endif + +
+ @lang('forms.cancel_action') + +
+
+
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/themes/base/admin/edit_storage.blade.php b/resources/views/themes/base/admin/edit_storage.blade.php index 9854f68..c77ace0 100644 --- a/resources/views/themes/base/admin/edit_storage.blade.php +++ b/resources/views/themes/base/admin/edit_storage.blade.php @@ -66,8 +66,13 @@ @include(Theme::viewName('partials.admin_storages_backblaze_b2_options')) @endif + @if ($storage->source == 'DropboxSource') +
+ @include(Theme::viewName('partials.admin_storages_dropbox_options')) + @endif +
- @lang('forms.cancel_action') + @lang('forms.cancel_action')
diff --git a/resources/views/themes/base/admin/list_services.blade.php b/resources/views/themes/base/admin/list_services.blade.php new file mode 100644 index 0000000..11253d9 --- /dev/null +++ b/resources/views/themes/base/admin/list_services.blade.php @@ -0,0 +1,55 @@ +@extends(Theme::viewName('layout')) +@section('title', trans('admin.services_title')) + +@section('breadcrumb') + + + +@endsection + +@section('content') +
+
+
+

@lang('admin.list_services_title')

+
+ @lang('admin.list_services_intro') +
+ + @if (count($services) == 0) +
+

@lang('admin.no_services_title')

+

@lang('admin.no_services_text')

+
+ @else + + + @foreach ($services as $service) + + + + + @endforeach + +
+ {{ $service->name }}
+ {{ trans(sprintf('services.%s', $service->service_type)) }} + @if ($service->service_type == \App\ExternalService::DROPBOX) + · {{ decrypt($service->app_id) }} + @endif +
+ @lang('forms.delete_action') +
+ +
+ {{ $services->links() }} +
+ @endif + + +
+
+
+@endsection \ No newline at end of file diff --git a/resources/views/themes/base/admin/list_storage.blade.php b/resources/views/themes/base/admin/list_storage.blade.php index 1ff4bee..0af9467 100644 --- a/resources/views/themes/base/admin/list_storage.blade.php +++ b/resources/views/themes/base/admin/list_storage.blade.php @@ -38,16 +38,24 @@
@if ($storage->source == 'LocalFilesystemSource'){{ $storage->location }}@endif - @if ($storage->source == 'OpenStackSource'){{ $storage->container_name }} - {{ $storage->service_name }}, {{ $storage->service_region }}@endif + @if ($storage->source == 'OpenStackSource'){{ $storage->container_name }} · {{ $storage->service_name }}, {{ $storage->service_region }}@endif @if ($storage->source == 'AmazonS3Source') - {{ $storage->container_name }} - {{ $storage->service_region }} + {{ $storage->container_name }} · {{ $storage->service_region }} @if ($storage->s3_signed_urls) @endif @endif - @if ($storage->source == 'RackspaceSource'){{ $storage->container_name }} - {{ $storage->service_region }}@endif + @if ($storage->source == 'RackspaceSource'){{ $storage->container_name }} · {{ $storage->service_region }}@endif + @if ($storage->source == 'DropboxSource') + @if (empty($storage->access_token)) + @lang('admin.storage_authorise_external_service_required') + @else + @lang('admin.storage_authorise_external_service_authorised') · @lang('admin.storage_authorise_external_service_refresh_authentication') + @endif + @endif +

@if (!$storage->is_internal) @lang('forms.delete_action') diff --git a/resources/views/themes/base/partials/admin_services_oauth_options.blade.php b/resources/views/themes/base/partials/admin_services_oauth_options.blade.php new file mode 100644 index 0000000..ac2db65 --- /dev/null +++ b/resources/views/themes/base/partials/admin_services_oauth_options.blade.php @@ -0,0 +1,26 @@ +
+
+
+ + + + @if ($errors->has('app_id')) +
+ {{ $errors->first('app_id') }} +
+ @endif +
+
+
+
+ + + + @if ($errors->has('app_secret')) +
+ {{ $errors->first('app_secret') }} +
+ @endif +
+
+
\ No newline at end of file diff --git a/resources/views/themes/base/partials/admin_storages_dropbox_options.blade.php b/resources/views/themes/base/partials/admin_storages_dropbox_options.blade.php index 41c2577..3ccf89b 100644 --- a/resources/views/themes/base/partials/admin_storages_dropbox_options.blade.php +++ b/resources/views/themes/base/partials/admin_storages_dropbox_options.blade.php @@ -1,41 +1,16 @@ -
-
-
- - +
+ + + + @if ($errors->has('external_service_id')) +
+ {{ $errors->first('external_service_id') }}
-
-
-
- - - - @if ($errors->has('secret_key')) -
- {{ $errors->first('secret_key') }} -
- @endif -
-
-
- -
-
-
- - - - @if ($errors->has('access_key')) -
- {{ $errors->first('access_token') }} -
- @endif -
-
+ @endif
\ No newline at end of file diff --git a/resources/views/themes/base/partials/copyright_admin.blade.php b/resources/views/themes/base/partials/copyright_admin.blade.php index af4d48c..4a8b1fd 100644 --- a/resources/views/themes/base/partials/copyright_admin.blade.php +++ b/resources/views/themes/base/partials/copyright_admin.blade.php @@ -5,7 +5,7 @@

@lang('global.powered_by', [ - 'link_start' => '', + 'link_start' => '', 'link_end' => '', ])
diff --git a/resources/views/themes/base/partials/copyright_gallery.blade.php b/resources/views/themes/base/partials/copyright_gallery.blade.php index 91f1066..d6e3982 100644 --- a/resources/views/themes/base/partials/copyright_gallery.blade.php +++ b/resources/views/themes/base/partials/copyright_gallery.blade.php @@ -5,7 +5,7 @@

@lang('global.powered_by', [ - 'link_start' => '', + 'link_start' => '', 'link_end' => '', ])
diff --git a/routes/web.php b/routes/web.php index 5641595..7529b8f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -55,6 +55,8 @@ Route::group(['prefix' => 'admin'], function () { Route::resource('labels', 'Admin\LabelController'); // 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'); @@ -81,7 +83,7 @@ Route::group(['prefix' => 'admin'], function () { Route::resource('comments', 'Admin\PhotoCommentController'); // Services management - Route::get('services/{services}/delete', 'Admin\ServiceController@delete')->name('services.delete'); + Route::get('services/{service}/delete', 'Admin\ServiceController@delete')->name('services.delete'); Route::resource('services', 'Admin\ServiceController'); });