Update Dropbox storage locations to support short-lived OAuth-compatible tokens.
This commit is contained in:
parent
59adca779e
commit
12b708a164
|
@ -48,6 +48,7 @@
|
|||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"league/flysystem-sftp-v3": "^3.0",
|
||||
"league/mime-type-detection": "^1.7",
|
||||
"league/oauth2-client": "^2.6",
|
||||
"league/plates": "^3.1",
|
||||
"lstrojny/fxmlrpc": "dev-master",
|
||||
"marcw/rss-writer": "^0.4.0",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "1231ed6704dea3766d3014a14233b3cb",
|
||||
"content-hash": "fe3a67cfc6f90897593aef83f8a54767",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
|
@ -3401,6 +3401,76 @@
|
|||
],
|
||||
"time": "2022-04-17T13:12:02+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/oauth2-client",
|
||||
"version": "2.6.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/oauth2-client.git",
|
||||
"reference": "2334c249907190c132364f5dae0287ab8666aa19"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/2334c249907190c132364f5dae0287ab8666aa19",
|
||||
"reference": "2334c249907190c132364f5dae0287ab8666aa19",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"guzzlehttp/guzzle": "^6.0 || ^7.0",
|
||||
"paragonie/random_compat": "^1 || ^2 || ^9.99",
|
||||
"php": "^5.6 || ^7.0 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.3.5",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.3.1",
|
||||
"phpunit/phpunit": "^5.7 || ^6.0 || ^9.5",
|
||||
"squizlabs/php_codesniffer": "^2.3 || ^3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-2.x": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\OAuth2\\Client\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alex Bilbie",
|
||||
"email": "hello@alexbilbie.com",
|
||||
"homepage": "http://www.alexbilbie.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Woody Gilk",
|
||||
"homepage": "https://github.com/shadowhand",
|
||||
"role": "Contributor"
|
||||
}
|
||||
],
|
||||
"description": "OAuth 2.0 Client Library",
|
||||
"keywords": [
|
||||
"Authentication",
|
||||
"SSO",
|
||||
"authorization",
|
||||
"identity",
|
||||
"idp",
|
||||
"oauth",
|
||||
"oauth2",
|
||||
"single sign on"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/oauth2-client/issues",
|
||||
"source": "https://github.com/thephpleague/oauth2-client/tree/2.6.1"
|
||||
},
|
||||
"time": "2021-12-22T16:42:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/plates",
|
||||
"version": "v3.5.0",
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<section class="card mt-3">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
{{ $gettext('Remote: Dropbox') }}
|
||||
</h2>
|
||||
</div>
|
||||
<b-card-body>
|
||||
<b-form-group>
|
||||
<div class="form-row">
|
||||
<div class="col-md-12">
|
||||
<h3>{{ $gettext('Dropbox Setup Instructions') }}</h3>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
{{ $gettext('Visit the Dropbox App Console:') }}<br>
|
||||
<a
|
||||
href="https://www.dropbox.com/developers/apps"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $gettext('Dropbox App Console') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
$gettext('Create a new application. Choose "Scoped Access", select your preferred level of access, then name your app. Do not name it "AzuraCast", but rather use a name specific to your installation.')
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{ $gettext('Enter your app secret and app key below.') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<b-wrapped-form-group
|
||||
id="form_edit_dropboxAppKey"
|
||||
class="col-md-6"
|
||||
:field="form.dropboxAppKey"
|
||||
>
|
||||
<template #label>
|
||||
{{ $gettext('App Key') }}
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<b-wrapped-form-group
|
||||
id="form_edit_dropboxAppSecret"
|
||||
class="col-md-6"
|
||||
:field="form.dropboxAppSecret"
|
||||
>
|
||||
<template #label>
|
||||
{{ $gettext('App Secret') }}
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
|
||||
<div class="col-md-12">
|
||||
<ul>
|
||||
<li>
|
||||
{{ $gettext('Visit the link below to sign in and generate an access code:') }}<br>
|
||||
<a
|
||||
:href="authUrl"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $gettext('Generate Access Code') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ $gettext('Enter the authorization code you receive below.') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<b-wrapped-form-group
|
||||
id="form_edit_dropboxAuthToken"
|
||||
class="col-md-12"
|
||||
:field="form.dropboxAuthToken"
|
||||
>
|
||||
<template #label>
|
||||
{{ $gettext('Access Token') }}
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</div>
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup.vue";
|
||||
import {computed} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const baseAuthUrl = 'https://www.dropbox.com/oauth2/authorize';
|
||||
|
||||
const authUrl = computed(() => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('client_id', props.form.dropboxAppKey.$model);
|
||||
params.append('response_type', 'code');
|
||||
params.append('token_access_type', 'offline');
|
||||
|
||||
return baseAuthUrl + '?' + params.toString();
|
||||
});
|
||||
</script>
|
|
@ -57,6 +57,8 @@ const {
|
|||
's3Version': {},
|
||||
's3Bucket': {},
|
||||
's3Endpoint': {},
|
||||
'dropboxAppKey': {},
|
||||
'dropboxAppSecret': {},
|
||||
'dropboxAuthToken': {},
|
||||
'sftpHost': {},
|
||||
'sftpPort': {},
|
||||
|
|
|
@ -140,37 +140,10 @@
|
|||
</b-card-body>
|
||||
</b-card>
|
||||
|
||||
<b-card
|
||||
<dropbox
|
||||
v-show="form.adapter.$model === 'dropbox'"
|
||||
class="mb-3"
|
||||
no-body
|
||||
>
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
{{ $gettext('Remote: Dropbox') }}
|
||||
</h2>
|
||||
</div>
|
||||
<b-card-body>
|
||||
<b-form-group>
|
||||
<div class="form-row">
|
||||
<b-wrapped-form-group
|
||||
id="form_edit_dropboxAuthToken"
|
||||
class="col-md-12"
|
||||
:field="form.dropboxAuthToken"
|
||||
>
|
||||
<template #label>
|
||||
{{ $gettext('Dropbox Generated Access Token') }}
|
||||
</template>
|
||||
<template #description>
|
||||
{{
|
||||
$gettext('Note: Dropbox now only issues short-lived tokens that will not work for this purpose. If your token begins with "sl", it is short-lived and will not work correctly.')
|
||||
}}
|
||||
</template>
|
||||
</b-wrapped-form-group>
|
||||
</div>
|
||||
</b-form-group>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
:form="form"
|
||||
/>
|
||||
|
||||
<b-card
|
||||
v-show="form.adapter.$model === 'sftp'"
|
||||
|
@ -256,6 +229,7 @@
|
|||
|
||||
<script setup>
|
||||
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup.vue";
|
||||
import Dropbox from "./Dropbox.vue";
|
||||
|
||||
const props = defineProps({
|
||||
form: {
|
||||
|
|
|
@ -25,6 +25,11 @@ parameters:
|
|||
count: 1
|
||||
path: src/Entity/Migration/Version20201204043539.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$accessTokenOrAppCredentials of class Spatie\\\\Dropbox\\\\Client constructor expects array\\|string\\|null, App\\\\Service\\\\Dropbox\\\\OAuthAdapter given\\.$#"
|
||||
count: 1
|
||||
path: src/Entity/StorageLocationAdapter/DropboxStorageLocationAdapter.php
|
||||
|
||||
-
|
||||
message: "#^Cannot cast Symfony\\\\Component\\\\Validator\\\\ConstraintViolationListInterface to string\\.$#"
|
||||
count: 1
|
||||
|
|
|
@ -74,6 +74,18 @@ final class StorageLocation
|
|||
)]
|
||||
public ?string $s3Endpoint = null;
|
||||
|
||||
#[OA\Property(
|
||||
description: 'The optional Dropbox App Key.',
|
||||
example: ''
|
||||
)]
|
||||
public ?string $dropboxAppKey = null;
|
||||
|
||||
#[OA\Property(
|
||||
description: 'The optional Dropbox App Secret.',
|
||||
example: ''
|
||||
)]
|
||||
public ?string $dropboxAppSecret = null;
|
||||
|
||||
#[OA\Property(
|
||||
description: 'The optional Dropbox Auth Token.',
|
||||
example: ''
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity\Migration;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230410210554 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add new Dropbox-related keys.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE storage_location ADD dropbox_app_key VARCHAR(50) DEFAULT NULL, ADD dropbox_app_secret VARCHAR(150) DEFAULT NULL, ADD dropbox_refresh_token VARCHAR(255) DEFAULT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE storage_location DROP dropbox_app_key, DROP dropbox_app_secret, DROP dropbox_refresh_token');
|
||||
}
|
||||
}
|
|
@ -56,9 +56,18 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
|
|||
#[ORM\Column(name: 's3_endpoint', length: 255, nullable: true)]
|
||||
protected ?string $s3Endpoint = null;
|
||||
|
||||
#[ORM\Column(name: 'dropbox_app_key', length: 50, nullable: true)]
|
||||
protected ?string $dropboxAppKey = null;
|
||||
|
||||
#[ORM\Column(name: 'dropbox_app_secret', length: 150, nullable: true)]
|
||||
protected ?string $dropboxAppSecret = null;
|
||||
|
||||
#[ORM\Column(name: 'dropbox_auth_token', length: 255, nullable: true)]
|
||||
protected ?string $dropboxAuthToken = null;
|
||||
|
||||
#[ORM\Column(name: 'dropbox_refresh_token', length: 255, nullable: true)]
|
||||
protected ?string $dropboxRefreshToken = null;
|
||||
|
||||
#[ORM\Column(name: 'sftp_host', length: 255, nullable: true)]
|
||||
protected ?string $sftpHost = null;
|
||||
|
||||
|
@ -195,6 +204,26 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
|
|||
$this->s3Endpoint = $this->truncateNullableString($s3Endpoint);
|
||||
}
|
||||
|
||||
public function getDropboxAppKey(): ?string
|
||||
{
|
||||
return $this->dropboxAppKey;
|
||||
}
|
||||
|
||||
public function setDropboxAppKey(?string $dropboxAppKey): void
|
||||
{
|
||||
$this->dropboxAppKey = $dropboxAppKey;
|
||||
}
|
||||
|
||||
public function getDropboxAppSecret(): ?string
|
||||
{
|
||||
return $this->dropboxAppSecret;
|
||||
}
|
||||
|
||||
public function setDropboxAppSecret(?string $dropboxAppSecret): void
|
||||
{
|
||||
$this->dropboxAppSecret = $dropboxAppSecret;
|
||||
}
|
||||
|
||||
public function getDropboxAuthToken(): ?string
|
||||
{
|
||||
return $this->dropboxAuthToken;
|
||||
|
@ -205,6 +234,16 @@ class StorageLocation implements Stringable, IdentifiableEntityInterface
|
|||
$this->dropboxAuthToken = $dropboxAuthToken;
|
||||
}
|
||||
|
||||
public function getDropboxRefreshToken(): ?string
|
||||
{
|
||||
return $this->dropboxRefreshToken;
|
||||
}
|
||||
|
||||
public function setDropboxRefreshToken(?string $dropboxRefreshToken): void
|
||||
{
|
||||
$this->dropboxRefreshToken = $dropboxRefreshToken;
|
||||
}
|
||||
|
||||
public function getSftpHost(): ?string
|
||||
{
|
||||
return $this->sftpHost;
|
||||
|
|
|
@ -8,10 +8,16 @@ use App\Entity\Enums\StorageLocationAdapters;
|
|||
use App\Entity\StorageLocation;
|
||||
use App\Flysystem\Adapter\DropboxAdapter;
|
||||
use App\Flysystem\Adapter\ExtendedAdapterInterface;
|
||||
use App\Service\Dropbox\OAuthAdapter;
|
||||
use Spatie\Dropbox\Client;
|
||||
|
||||
final class DropboxStorageLocationAdapter extends AbstractStorageLocationLocationAdapter
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OAuthAdapter $oauthAdapter
|
||||
) {
|
||||
}
|
||||
|
||||
public function getType(): StorageLocationAdapters
|
||||
{
|
||||
return StorageLocationAdapters::Dropbox;
|
||||
|
@ -26,7 +32,15 @@ final class DropboxStorageLocationAdapter extends AbstractStorageLocationLocatio
|
|||
|
||||
private function getClient(): Client
|
||||
{
|
||||
return new Client($this->storageLocation->getDropboxAuthToken());
|
||||
return new Client($this->oauthAdapter->withStorageLocation($this->storageLocation));
|
||||
}
|
||||
|
||||
public function validate(): void
|
||||
{
|
||||
$adapter = $this->oauthAdapter->withStorageLocation($this->storageLocation);
|
||||
$adapter->setup();
|
||||
|
||||
parent::validate();
|
||||
}
|
||||
|
||||
public static function filterPath(string $path): string
|
||||
|
@ -40,9 +54,9 @@ final class DropboxStorageLocationAdapter extends AbstractStorageLocationLocatio
|
|||
|
||||
$token = (!empty($storageLocation->getDropboxAuthToken()))
|
||||
? $storageLocation->getDropboxAuthToken()
|
||||
: '';
|
||||
: $storageLocation->getDropboxRefreshToken();
|
||||
|
||||
$token = substr(md5($token), 0, 10);
|
||||
$token = substr(md5($token ?? ''), 0, 10);
|
||||
|
||||
return 'dropbox://' . $token . '/' . ltrim($path, '/');
|
||||
}
|
||||
|
|
|
@ -4,6 +4,95 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Service\Dropbox;
|
||||
|
||||
final class OAuthAdapter
|
||||
use App\Entity\StorageLocation;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Spatie\Dropbox\RefreshableTokenProvider;
|
||||
|
||||
final class OAuthAdapter implements RefreshableTokenProvider
|
||||
{
|
||||
private StorageLocation $storageLocation;
|
||||
|
||||
public function __construct(
|
||||
private readonly CacheItemPoolInterface $psr6Cache
|
||||
) {
|
||||
}
|
||||
|
||||
public function withStorageLocation(StorageLocation $storageLocation): self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->setStorageLocation($storageLocation);
|
||||
return $clone;
|
||||
}
|
||||
|
||||
private function setStorageLocation(StorageLocation $storageLocation): void
|
||||
{
|
||||
$this->storageLocation = $storageLocation;
|
||||
}
|
||||
|
||||
public function setup(): void
|
||||
{
|
||||
$this->psr6Cache->deleteItem($this->getTokenCacheKey());
|
||||
|
||||
if (!empty($this->storageLocation->getDropboxAuthToken())) {
|
||||
// Convert the short-lived auth code into an oauth refresh token.
|
||||
$token = $this->getOauthProvider()->getAccessToken(
|
||||
'authorization_code',
|
||||
[
|
||||
'code' => $this->storageLocation->getDropboxAuthToken(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->storageLocation->setDropboxAuthToken(null);
|
||||
$this->storageLocation->setDropboxRefreshToken($token->getRefreshToken());
|
||||
}
|
||||
}
|
||||
|
||||
public function refresh(ClientException $exception): bool
|
||||
{
|
||||
$this->psr6Cache->deleteItem($this->getTokenCacheKey());
|
||||
$this->getToken();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getToken(): string
|
||||
{
|
||||
$cacheKey = $this->getTokenCacheKey();
|
||||
$cacheItem = $this->psr6Cache->getItem($cacheKey);
|
||||
|
||||
if (!$cacheItem->isHit()) {
|
||||
if (empty($this->storageLocation->getDropboxRefreshToken())) {
|
||||
$cacheItem->set($this->storageLocation->getDropboxAuthToken());
|
||||
} else {
|
||||
// Try to get a new auth token from the refresh token.
|
||||
$token = $this->getOauthProvider()->getAccessToken(
|
||||
'refresh_token',
|
||||
[
|
||||
'refresh_token' => $this->storageLocation->getDropboxRefreshToken(),
|
||||
]
|
||||
);
|
||||
|
||||
$cacheItem->set($token->getToken());
|
||||
}
|
||||
|
||||
$cacheItem->expiresAfter(600);
|
||||
$this->psr6Cache->saveDeferred($cacheItem);
|
||||
}
|
||||
|
||||
return $cacheItem->get();
|
||||
}
|
||||
|
||||
private function getOauthProvider(): OAuthProvider
|
||||
{
|
||||
return new OAuthProvider([
|
||||
'clientId' => $this->storageLocation->getDropboxAppKey(),
|
||||
'clientSecret' => $this->storageLocation->getDropboxAppSecret(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function getTokenCacheKey(): string
|
||||
{
|
||||
return 'storage_location_' . ($this->storageLocation->getId() ?? 'new') . '_auth_token';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\Dropbox;
|
||||
|
||||
use League\OAuth2\Client\Provider\AbstractProvider;
|
||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||
use League\OAuth2\Client\Token\AccessToken;
|
||||
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class OAuthProvider extends AbstractProvider
|
||||
{
|
||||
use BearerAuthorizationTrait;
|
||||
|
||||
/**
|
||||
* @var string Key used in the access token response to identify the resource owner.
|
||||
*/
|
||||
public const ACCESS_TOKEN_RESOURCE_OWNER_ID = 'account_id';
|
||||
|
||||
public function getBaseAuthorizationUrl(): string
|
||||
{
|
||||
return 'https://www.dropbox.com/oauth2/authorize';
|
||||
}
|
||||
|
||||
public function getBaseAccessTokenUrl(array $params): string
|
||||
{
|
||||
return 'https://api.dropbox.com/oauth2/token';
|
||||
}
|
||||
|
||||
public function getResourceOwnerDetailsUrl(AccessToken $token): string
|
||||
{
|
||||
return 'https://api.dropbox.com/2/users/get_current_account';
|
||||
}
|
||||
|
||||
protected function getDefaultScopes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a provider response for errors.
|
||||
*
|
||||
* @link https://www.dropbox.com/developers/core/docs
|
||||
* @throws IdentityProviderException
|
||||
* @param ResponseInterface $response
|
||||
* @param array|string $data Parsed response data
|
||||
* @return void
|
||||
*/
|
||||
protected function checkResponse(ResponseInterface $response, $data)
|
||||
{
|
||||
if (isset($data['error'])) {
|
||||
throw new IdentityProviderException(
|
||||
$data['error'] ?: $response->getReasonPhrase(),
|
||||
$response->getStatusCode(),
|
||||
(string)$response->getBody()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function createResourceOwner(array $response, AccessToken $token)
|
||||
{
|
||||
throw new \LogicException('Not implemented.');
|
||||
}
|
||||
}
|
|
@ -13,11 +13,15 @@ trait LoadFromParentObject
|
|||
{
|
||||
if (is_object($obj)) {
|
||||
foreach (get_object_vars($obj) as $key => $value) {
|
||||
$this->$key = $value;
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
} elseif (is_array($obj)) {
|
||||
foreach ($obj as $key => $value) {
|
||||
$this->$key = $value;
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue