From aa9500f5ad5354bae5cb81d252b359965e363cf6 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 9 Dec 2017 12:23:28 +0100 Subject: [PATCH] Initial code to get providers from Spectre. --- app/Jobs/GetSpectreProviders.php | 77 ++++ app/Models/SpectreProvider.php | 32 ++ app/Services/Bunq/Request/BunqRequest.php | 3 + .../Spectre/Request/ListProvidersRequest.php | 80 ++++ .../Spectre/Request/SpectreRequest.php | 378 ++++++++++++++++++ .../Prerequisites/SpectrePrerequisites.php | 229 +++++++++++ config/firefly.php | 44 +- .../2017_12_09_111046_changes_for_spectre.php | 47 +++ public/images/logos/bunq.png | Bin 1605 -> 3292 bytes public/images/logos/csv.png | Bin 0 -> 3515 bytes public/images/logos/plaid.png | Bin 0 -> 8326 bytes public/images/logos/spectre.png | Bin 0 -> 3446 bytes resources/lang/en_US/bank.php | 10 +- resources/lang/en_US/form.php | 4 + resources/views/import/index.twig | 29 +- .../views/import/spectre/prerequisites.twig | 58 +++ 16 files changed, 964 insertions(+), 27 deletions(-) create mode 100644 app/Jobs/GetSpectreProviders.php create mode 100644 app/Models/SpectreProvider.php create mode 100644 app/Services/Spectre/Request/ListProvidersRequest.php create mode 100644 app/Services/Spectre/Request/SpectreRequest.php create mode 100644 app/Support/Import/Prerequisites/SpectrePrerequisites.php create mode 100644 database/migrations/2017_12_09_111046_changes_for_spectre.php create mode 100644 public/images/logos/csv.png create mode 100644 public/images/logos/plaid.png create mode 100644 public/images/logos/spectre.png create mode 100644 resources/views/import/spectre/prerequisites.twig diff --git a/app/Jobs/GetSpectreProviders.php b/app/Jobs/GetSpectreProviders.php new file mode 100644 index 0000000000..0184d6ce4f --- /dev/null +++ b/app/Jobs/GetSpectreProviders.php @@ -0,0 +1,77 @@ +user = $user; + Log::debug('Constructed job GetSpectreProviders'); + } + + /** + * Execute the job. + */ + public function handle() + { + /** @var Configuration $configValue */ + $configValue = app('fireflyconfig')->get('spectre_provider_download', 0); + $now = time(); + if ($now - intval($configValue->data) < 86400) { + Log::debug(sprintf('Difference is %d, so will NOT execute job.', ($now - intval($configValue->data)))); + + return; + } + Log::debug(sprintf('Difference is %d, so will execute job.', ($now - intval($configValue->data)))); + + // get user + + // fire away! + $request = new ListProvidersRequest($this->user); + $request->call(); + + // store all providers: + $providers = $request->getProviders(); + foreach ($providers as $provider) { + // find provider? + $dbProvider = SpectreProvider::where('spectre_id', $provider['id'])->first(); + if (is_null($dbProvider)) { + $dbProvider = new SpectreProvider; + } + // update fields: + $dbProvider->spectre_id = $provider['id']; + $dbProvider->code = $provider['code']; + $dbProvider->mode = $provider['mode']; + $dbProvider->status = $provider['status']; + $dbProvider->interactive = $provider['interactive'] === 1; + $dbProvider->automatic_fetch = $provider['automatic_fetch'] === 1; + $dbProvider->country_code = $provider['country_code']; + $dbProvider->data = $provider; + $dbProvider->save(); + Log::debug(sprintf('Stored provider #%d under ID #%d', $provider['id'], $dbProvider->id)); + } + + app('fireflyconfig')->set('spectre_provider_download', time()); + + return; + } +} diff --git a/app/Models/SpectreProvider.php b/app/Models/SpectreProvider.php new file mode 100644 index 0000000000..4cdf9e9d55 --- /dev/null +++ b/app/Models/SpectreProvider.php @@ -0,0 +1,32 @@ + 'int', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + 'interactive' => 'boolean', + 'automatic_fetch' => 'boolean', + 'data' => 'array', + ]; + + protected $fillable = ['spectre_id', 'code', 'mode', 'name', 'status', 'interactive', 'automatic_fetch', 'country_code', 'data']; + +} \ No newline at end of file diff --git a/app/Services/Bunq/Request/BunqRequest.php b/app/Services/Bunq/Request/BunqRequest.php index dcc4a23729..ca73d0443c 100644 --- a/app/Services/Bunq/Request/BunqRequest.php +++ b/app/Services/Bunq/Request/BunqRequest.php @@ -160,6 +160,9 @@ abstract class BunqRequest return $result; } + /** + * @return array + */ protected function getDefaultHeaders(): array { $userAgent = sprintf('FireflyIII v%s', config('firefly.version')); diff --git a/app/Services/Spectre/Request/ListProvidersRequest.php b/app/Services/Spectre/Request/ListProvidersRequest.php new file mode 100644 index 0000000000..a4abd1ad2c --- /dev/null +++ b/app/Services/Spectre/Request/ListProvidersRequest.php @@ -0,0 +1,80 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Request; + +use Log; + +/** + * Class ListUserRequest. + */ +class ListProvidersRequest extends SpectreRequest +{ + protected $providers = []; + + /** + * + */ + public function call(): void + { + $hasNextPage = true; + $nextId = 0; + while ($hasNextPage) { + Log::debug(sprintf('Now calling for next_id %d', $nextId)); + $parameters = ['include_fake_providers' => 'true', 'include_provider_fields' => 'true', 'from_id' => $nextId]; + $uri = '/api/v3/providers?' . http_build_query($parameters); + $response = $this->sendSignedSpectreGet($uri, []); + + // count entries: + Log::debug(sprintf('Found %d entries in data-array', count($response['data']))); + + // extract next ID + $hasNextPage = false; + if (isset($response['meta']['next_id']) && intval($response['meta']['next_id']) > $nextId) { + $hasNextPage = true; + $nextId = $response['meta']['next_id']; + Log::debug(sprintf('Next ID is now %d.', $nextId)); + } else { + Log::debug('No next page.'); + } + + // store providers: + foreach ($response['data'] as $providerArray) { + $providerId = $providerArray['id']; + $this->providers[$providerId] = $providerArray; + Log::debug(sprintf('Stored provider #%d', $providerId)); + } + } + + return; + } + + /** + * @return array + */ + public function getProviders(): array + { + return $this->providers; + } + + +} diff --git a/app/Services/Spectre/Request/SpectreRequest.php b/app/Services/Spectre/Request/SpectreRequest.php new file mode 100644 index 0000000000..a7ce46f4d5 --- /dev/null +++ b/app/Services/Spectre/Request/SpectreRequest.php @@ -0,0 +1,378 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Services\Spectre\Request; + +use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\User; +use Log; +use Requests; +use Requests_Exception; + +//use FireflyIII\Services\Bunq\Object\ServerPublicKey; + +/** + * Class BunqRequest. + */ +abstract class SpectreRequest +{ + /** @var string */ + protected $clientId = ''; + protected $expiresAt = 0; + /** @var ServerPublicKey */ + protected $serverPublicKey; + /** @var string */ + protected $serviceSecret = ''; + /** @var string */ + private $privateKey = ''; + /** @var string */ + private $server = ''; + /** @var User */ + private $user; + + /** + * SpectreRequest constructor. + */ + public function __construct(User $user) + { + $this->user = $user; + $this->server = config('firefly.spectre.server'); + $this->expiresAt = time() + 180; + $privateKey = app('preferences')->get('spectre_private_key', null); + $this->privateKey = $privateKey->data; + + // set client ID + $clientId = app('preferences')->get('spectre_client_id', null); + $this->clientId = $clientId->data; + + // set service secret + $serviceSecret = app('preferences')->get('spectre_service_secret', null); + $this->serviceSecret = $serviceSecret->data; + } + + /** + * + */ + abstract public function call(): void; + + /** + * @return string + */ + public function getClientId(): string + { + return $this->clientId; + } + + /** + * @param string $clientId + */ + public function setClientId(string $clientId): void + { + $this->clientId = $clientId; + } + + /** + * @return string + */ + public function getServer(): string + { + return $this->server; + } + + /** + * @return ServerPublicKey + */ + public function getServerPublicKey(): ServerPublicKey + { + return $this->serverPublicKey; + } + + /** + * @param ServerPublicKey $serverPublicKey + */ + public function setServerPublicKey(ServerPublicKey $serverPublicKey) + { + $this->serverPublicKey = $serverPublicKey; + } + + /** + * @return string + */ + public function getServiceSecret(): string + { + return $this->serviceSecret; + } + + /** + * @param string $serviceSecret + */ + public function setServiceSecret(string $serviceSecret): void + { + $this->serviceSecret = $serviceSecret; + } + + /** + * @param string $privateKey + */ + public function setPrivateKey(string $privateKey) + { + $this->privateKey = $privateKey; + } + + /** + * @param string $secret + */ + public function setSecret(string $secret) + { + $this->secret = $secret; + } + + /** + * @param string $method + * @param string $uri + * @param string $data + * + * @return string + * + * @throws FireflyException + */ + protected function generateSignature(string $method, string $uri, string $data): string + { + if (0 === strlen($this->privateKey)) { + throw new FireflyException('No private key present.'); + } + if ('get' === strtolower($method) || 'delete' === strtolower($method)) { + $data = ''; + } + // base64(sha1_signature(private_key, "Expires-at|request_method|original_url|post_body|md5_of_uploaded_file|"))) + // Prepare the signature + $toSign = $this->expiresAt . '|' . strtoupper($method) . '|' . $uri . '|' . $data . ''; // no file so no content there. + Log::debug(sprintf('String to sign: %s', $toSign)); + $signature = ''; + + // Sign the data + openssl_sign($toSign, $signature, $this->privateKey, OPENSSL_ALGO_SHA1); + $signature = base64_encode($signature); + + return $signature; + } + + /** + * @return array + */ + protected function getDefaultHeaders(): array + { + $userAgent = sprintf('FireflyIII v%s', config('firefly.version')); + + return [ + 'Client-Id' => $this->getClientId(), + 'Service-Secret' => $this->getServiceSecret(), + 'Accept' => 'application/json', + 'Content-type' => 'application/json', + 'Cache-Control' => 'no-cache', + 'User-Agent' => $userAgent, + 'Expires-at' => $this->expiresAt, + ]; + } + + /** + * @param string $uri + * @param array $headers + * + * @return array + * + * @throws Exception + */ + protected function sendSignedBunqDelete(string $uri, array $headers): array + { + if (0 === strlen($this->server)) { + throw new FireflyException('No bunq server defined'); + } + + $fullUri = $this->server . $uri; + $signature = $this->generateSignature('delete', $uri, $headers, ''); + $headers['X-Bunq-Client-Signature'] = $signature; + try { + $response = Requests::delete($fullUri, $headers); + } catch (Requests_Exception $e) { + return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()]]]; + } + + $body = $response->body; + $array = json_decode($body, true); + $responseHeaders = $response->headers->getAll(); + $statusCode = intval($response->status_code); + $array['ResponseHeaders'] = $responseHeaders; + $array['ResponseStatusCode'] = $statusCode; + + Log::debug(sprintf('Response to DELETE %s is %s', $fullUri, $body)); + if ($this->isErrorResponse($array)) { + $this->throwResponseError($array); + } + + if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) { + throw new FireflyException(sprintf('Could not verify signature for request to "%s"', $uri)); + } + + return $array; + } + + /** + * @param string $uri + * @param array $data + * @param array $headers + * + * @return array + * + * @throws Exception + */ + protected function sendSignedBunqPost(string $uri, array $data, array $headers): array + { + $body = json_encode($data); + $fullUri = $this->server . $uri; + $signature = $this->generateSignature('post', $uri, $headers, $body); + $headers['X-Bunq-Client-Signature'] = $signature; + try { + $response = Requests::post($fullUri, $headers, $body); + } catch (Requests_Exception $e) { + return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()]]]; + } + + $body = $response->body; + $array = json_decode($body, true); + $responseHeaders = $response->headers->getAll(); + $statusCode = intval($response->status_code); + $array['ResponseHeaders'] = $responseHeaders; + $array['ResponseStatusCode'] = $statusCode; + + if ($this->isErrorResponse($array)) { + $this->throwResponseError($array); + } + + if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) { + throw new FireflyException(sprintf('Could not verify signature for request to "%s"', $uri)); + } + + return $array; + } + + /** + * @param string $uri + * @param array $data + * @param array $headers + * + * @return array + * + * @throws Exception + */ + protected function sendSignedSpectreGet(string $uri, array $data): array + { + if (0 === strlen($this->server)) { + throw new FireflyException('No Spectre server defined'); + } + + $headers = $this->getDefaultHeaders(); + $body = json_encode($data); + $fullUri = $this->server . $uri; + $signature = $this->generateSignature('get', $fullUri, $body); + $headers['Signature'] = $signature; + + Log::debug('Final headers for spectre signed get request:', $headers); + try { + $response = Requests::get($fullUri, $headers); + } catch (Requests_Exception $e) { + throw new FireflyException(sprintf('Request Exception: %s', $e->getMessage())); + } + $statusCode = intval($response->status_code); + + if ($statusCode !== 200) { + throw new FireflyException(sprintf('Status code %d: %s', $statusCode, $response->body)); + } + + $body = $response->body; + $array = json_decode($body, true); + $responseHeaders = $response->headers->getAll(); + $array['ResponseHeaders'] = $responseHeaders; + $array['ResponseStatusCode'] = $statusCode; + + return $array; + } + + /** + * @param string $uri + * @param array $headers + * + * @return array + */ + protected function sendUnsignedBunqDelete(string $uri, array $headers): array + { + $fullUri = $this->server . $uri; + try { + $response = Requests::delete($fullUri, $headers); + } catch (Requests_Exception $e) { + return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()]]]; + } + $body = $response->body; + $array = json_decode($body, true); + $responseHeaders = $response->headers->getAll(); + $statusCode = $response->status_code; + $array['ResponseHeaders'] = $responseHeaders; + $array['ResponseStatusCode'] = $statusCode; + + if ($this->isErrorResponse($array)) { + $this->throwResponseError($array); + } + + return $array; + } + + /** + * @param string $uri + * @param array $data + * @param array $headers + * + * @return array + */ + protected function sendUnsignedBunqPost(string $uri, array $data, array $headers): array + { + $body = json_encode($data); + $fullUri = $this->server . $uri; + try { + $response = Requests::post($fullUri, $headers, $body); + } catch (Requests_Exception $e) { + return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()]]]; + } + $body = $response->body; + $array = json_decode($body, true); + $responseHeaders = $response->headers->getAll(); + $statusCode = $response->status_code; + $array['ResponseHeaders'] = $responseHeaders; + $array['ResponseStatusCode'] = $statusCode; + + if ($this->isErrorResponse($array)) { + $this->throwResponseError($array); + } + + return $array; + } +} diff --git a/app/Support/Import/Prerequisites/SpectrePrerequisites.php b/app/Support/Import/Prerequisites/SpectrePrerequisites.php new file mode 100644 index 0000000000..a55c0f87b6 --- /dev/null +++ b/app/Support/Import/Prerequisites/SpectrePrerequisites.php @@ -0,0 +1,229 @@ +. + */ +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Prerequisites; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Jobs\GetSpectreProviders; +use FireflyIII\Models\Configuration; +use FireflyIII\Models\Preference; +use FireflyIII\User; +use Illuminate\Http\Request; +use Illuminate\Support\MessageBag; +use Log; +use Preferences; +use Requests; +use Requests_Exception; + +/** + * This class contains all the routines necessary to connect to Bunq. + */ +class SpectrePrerequisites implements PrerequisitesInterface +{ + /** @var User */ + private $user; + + /** + * Returns view name that allows user to fill in prerequisites. Currently asks for the API key. + * + * @return string + */ + public function getView(): string + { + return 'import.spectre.prerequisites'; + } + + /** + * Returns any values required for the prerequisites-view. + * + * @return array + */ + public function getViewParameters(): array + { + $publicKey = $this->getPublicKey(); + $subTitle = strval(trans('bank.spectre_title')); + $subTitleIcon = 'fa-archive'; + + return compact('publicKey', 'subTitle', 'subTitleIcon'); + } + + /** + * Returns if this import method has any special prerequisites such as config + * variables or other things. The only thing we verify is the presence of the API key. Everything else + * tumbles into place: no installation token? Will be requested. No device server? Will be created. Etc. + * + * @return bool + */ + public function hasPrerequisites(): bool + { + $values = [ + Preferences::getForUser($this->user, 'spectre_client_id', false), + Preferences::getForUser($this->user, 'spectre_app_secret', false), + Preferences::getForUser($this->user, 'spectre_service_secret', false), + ]; + /** @var Preference $value */ + foreach ($values as $value) { + if (false === $value->data || null === $value->data) { + Log::info(sprintf('Config var "%s" is missing.', $value->name)); + + return true; + } + } + Log::debug('All prerequisites are here!'); + + // at this point, check if all providers are present. Providers are shared amongst + // users in a multi-user environment. + GetSpectreProviders::dispatch($this->user); + + return false; + } + + /** + * Set the user for this Prerequisites-routine. Class is expected to implement and save this. + * + * @param User $user + */ + public function setUser(User $user): void + { + $this->user = $user; + + return; + } + + /** + * This method responds to the user's submission of an API key. It tries to register this instance as a new Firefly III device. + * If this fails, the error is returned in a message bag and the user is notified (this is fairly friendly). + * + * @param Request $request + * + * @return MessageBag + */ + public function storePrerequisites(Request $request): MessageBag + { + Log::debug('Storing Spectre API keys..'); + Preferences::setForUser($this->user, 'spectre_client_id', $request->get('client_id')); + Preferences::setForUser($this->user, 'spectre_app_secret', $request->get('app_secret')); + Preferences::setForUser($this->user, 'spectre_service_secret', $request->get('service_secret')); + Log::debug('Done!'); + + return new MessageBag; + } + + /** + * This method creates a new public/private keypair for the user. This isn't really secure, since the key is generated on the fly with + * no regards for HSM's, smart cards or other things. It would require some low level programming to get this right. But the private key + * is stored encrypted in the database so it's something. + */ + private function createKeyPair(): void + { + Log::debug('Generate new Spectre key pair for user.'); + $keyConfig = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]; + // Create the private and public key + $res = openssl_pkey_new($keyConfig); + + // Extract the private key from $res to $privKey + $privKey = ''; + openssl_pkey_export($res, $privKey); + + // Extract the public key from $res to $pubKey + $pubKey = openssl_pkey_get_details($res); + + Preferences::setForUser($this->user, 'spectre_private_key', $privKey); + Preferences::setForUser($this->user, 'spectre_public_key', $pubKey['key']); + Log::debug('Created key pair'); + + return; + } + + /** + * Get the private key from the users preferences. + * + * @return string + */ + private function getPrivateKey(): string + { + Log::debug('get private key'); + $preference = Preferences::getForUser($this->user, 'spectre_private_key', null); + if (null === $preference) { + Log::debug('private key is null'); + // create key pair + $this->createKeyPair(); + } + $preference = Preferences::getForUser($this->user, 'spectre_private_key', null); + Log::debug('Return private key for user'); + + return $preference->data; + } + + /** + * Get a public key from the users preferences. + * + * @return string + */ + private function getPublicKey(): string + { + Log::debug('get public key'); + $preference = Preferences::getForUser($this->user, 'spectre_public_key', null); + if (null === $preference) { + Log::debug('public key is null'); + // create key pair + $this->createKeyPair(); + } + $preference = Preferences::getForUser($this->user, 'spectre_public_key', null); + Log::debug('Return public key for user'); + + return $preference->data; + } + + /** + * Request users server remote IP. Let's assume this value will not change any time soon. + * + * @return string + * + * @throws FireflyException + */ + private function getRemoteIp(): string + { + $preference = Preferences::getForUser($this->user, 'external_ip', null); + if (null === $preference) { + try { + $response = Requests::get('https://api.ipify.org'); + } catch (Requests_Exception $e) { + throw new FireflyException(sprintf('Could not retrieve external IP: %s', $e->getMessage())); + } + if (200 !== $response->status_code) { + throw new FireflyException(sprintf('Could not retrieve external IP: %d %s', $response->status_code, $response->body)); + } + $serverIp = $response->body; + Preferences::setForUser($this->user, 'external_ip', $serverIp); + + return $serverIp; + } + + return $preference->data; + } + +} diff --git a/config/firefly.php b/config/firefly.php index 744d82cfc3..b4219692d4 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -28,39 +28,49 @@ declare(strict_types=1); */ return [ - 'configuration' => [ + 'configuration' => [ 'single_user_mode' => true, 'is_demo_site' => false, ], - 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), - 'version' => '4.6.11.1', - 'maxUploadSize' => 15242880, - 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf','text/plain'], - 'list_length' => 10, - 'export_formats' => [ + 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), + 'version' => '4.6.11.1', + 'maxUploadSize' => 15242880, + 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf', 'text/plain'], + 'list_length' => 10, + 'export_formats' => [ 'csv' => 'FireflyIII\Export\Exporter\CsvExporter', ], - 'import_formats' => [ + 'import_formats' => [ 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', ], - 'import_configurators' => [ + 'import_configurators' => [ 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', ], - 'import_processors' => [ + 'import_processors' => [ 'csv' => 'FireflyIII\Import\FileProcessor\CsvProcessor', ], - 'import_pre' => [ - 'bunq' => 'FireflyIII\Support\Import\Prerequisites\BunqPrerequisites', + 'import_pre' => [ + 'bunq' => 'FireflyIII\Support\Import\Prerequisites\BunqPrerequisites', + 'spectre' => 'FireflyIII\Support\Import\Prerequisites\SpectrePrerequisites', + 'plaid' => 'FireflyIII\Support\Import\Prerequisites\PlairPrerequisites', ], - 'import_info' => [ - 'bunq' => 'FireflyIII\Support\Import\Information\BunqInformation', + 'import_info' => [ + 'bunq' => 'FireflyIII\Support\Import\Information\BunqInformation', + 'spectre' => 'FireflyIII\Support\Import\Information\SpectreInformation', + 'plaid' => 'FireflyIII\Support\Import\Information\PlaidInformation', ], - 'import_transactions' => [ - 'bunq' => 'FireflyIII\Support\Import\Transactions\BunqTransactions', + 'import_transactions' => [ + 'bunq' => 'FireflyIII\Support\Import\Transactions\BunqTransactions', + 'spectre' => 'FireflyIII\Support\Import\Transactions\SpectreTransactions', + 'plaid' => 'FireflyIII\Support\Import\Transactions\PlaidTransactions', ], - 'bunq' => [ + 'bunq' => [ 'server' => 'https://sandbox.public.api.bunq.com', ], + 'spectre' => [ + 'server' => 'https://www.saltedge.com', + ], + 'default_export_format' => 'csv', 'default_import_format' => 'csv', 'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], diff --git a/database/migrations/2017_12_09_111046_changes_for_spectre.php b/database/migrations/2017_12_09_111046_changes_for_spectre.php new file mode 100644 index 0000000000..ff25ce1aef --- /dev/null +++ b/database/migrations/2017_12_09_111046_changes_for_spectre.php @@ -0,0 +1,47 @@ +increments('id'); + $table->timestamps(); + $table->softDeletes(); + //'spectre_id', 'code', 'mode', 'name', 'status', 'interactive', 'automatic_fetch', 'country_code', 'data' + $table->integer('spectre_id', false, true); + $table->string('code', 100); + $table->string('mode', 20); + $table->string('status', 20); + $table->boolean('interactive')->default(0); + $table->boolean('automatic_fetch')->default(0); + $table->string('country_code', 3); + $table->text('data'); + } + ); + } + } +} diff --git a/public/images/logos/bunq.png b/public/images/logos/bunq.png index c1223bc4fc2ad3cbc6fda0603369137f43bc60fd..fad92342b79678a336fa1396060bc0d3f44d4cad 100644 GIT binary patch delta 2453 zcmZ`*dpOgJ8#kBCq7-r$lKZhuF2g9AyWfb(k`80bT$mlJp>pUThy2`0 zC}B$dLavQMWNx|Jil6hxd7ksnc|XtlzMtpwd7t-r|9rbeCuLQBb3xmRLF6EOe0*Z| zb|}}wj{GaZM-Hp$k1CG?1G&tm}L+&FW#MBiK2N^NV z1+IG;!H`e_J>a^jO8}h^d<7M7xQH4MLb8A;{~buYpDFdGZy>>s_7C;YMN;tBP2C8g z(0|Ct!+ZXoW>$bsA2)^To5B#v`mn!<35+=kGC66Cgwk)2O)pXiI9dn=6A}{4Y!xs9 z(yqow@$rG(>`|5&qR{eH1{pIfdp?~vYO5q}#|M8~3GyVK$S<>#wMvLPCMs8IADpTE z<)P2BM?$BoI>xo#hAi{%qBwjI_a~3y<6X_%{FM>P1fSP-;A+8$1VMp5b-Z!w&dT%o z2QlFm%>>tkvop+uKdna1m9`&)$jUwBv%x%^bno45Z5z@g~yZ8j@#eR5@t?Ddne- zb#{+#Y4bW~xhR?4Q(F4HuG(Y|MZ$6-nA!;~*5g!1jbMupA@$Y9K|vd*yM`F)zDM$R zEIgYOmb#tetV0S}L5LW-yC$8W1k=wKQDEUebEUZ78i76b-SF5lQ0&pkD~A4Y8obIL zr-9UmEA)=*7xd&JQTVSTKEQLo$6rJShWLG3TNehyeZQ+!!^kfM-7F)?t1fe&T-_VOo)ev)B5gBQ4_s%G{o!e_Rh^zBN<+wCaF zPl?QgKJGVC(jp;7F3IC5=vT8TUAOX34rxU0CY$0XZzfo!2+;0EplgW17|-Io zX8Q*lQ0iQOrmxTM=Yn0eYbDc-pbCNc$$~;^M^1?jPt3@Wj~9s6SyD9Ze-i5$vy-Rs zw+oRU?(I>H-8hJO%@l_A`2)&Sf5*k*b~S{s&^i92oji|$6|nMR*HBf|N57f&Yu@;J zujNmm&abh*1qod#2RNxcUr(hspP}w+9{+j!Xk63Oi>F&`M(%4`hIrGB;fnJWokawh zyZ184itp!c3zH++sIp@P<=+;=Ad@~P8jzlWHeS-EeLAA?C{mhQ?OSDbtb)Y&tz7g;i0z!}{j)b@wyAjd$zb!m3= z&a{UA&gxue$2Cunoz<|A(VCfd|00pP%b44r_XnQH`1XP$ECw_SFUM)Lk29{V3Loq#pr|?Ybr4g7W zX-@A-b-{<+n)6@$o}o|rI8~`cbr=IunAX{bP1{P2kaQ0`1*fKd)^U<$cMX#QHxQJ# z)zsntX!_L-@y#ul?Efk##Od%C(DDqA*;|aIX5c7a%>c(!g{ghx#q>5Pn6WFKMFNpA zm)7V)_42h`Xd55e+I!fbS@vED**so=F9xQnzmy!8zhHJ(Ie{(-ll+9Q@nCj%s%th7 zmxGqL*X(@cJ|+1il&r7+5##iFKY@;Y@nrvgQ|D-gj|&~#KCu&e(=+(Z{+pJAIV;#^ zv<}7!AZFum55=cb#G7tI2J0%wRuUGdj>QE{q|*=Z+nN<>=(I@J!Msray2#P9_>s|% z!l#!UH{@w3w|dB2vy5zClp3hvqvedv_KN`p>@OKmTzkXi_cP0PI0^VjYr#%J!dyHYqtb3{^po4lmz?e{u=pe ziu+icmjwxFV(f%SjwuMw2W6G2;T*eILwf9uAMe;Lm?l;K|J=!?d!oY%6@Ru|ZjQua z3yKXZ1nM-JAvo2O@a8vM2ilD;uLGyI6GVO_w&d zd{=DYyGX&o_6XNDhR7PP4O4l$2rMoa4l6;DoMaiI?pFG~os$MFZILm*x*-Ks@Cbpb zf@4@TNpKssk`q@k)YU@u0O+cw`u{zuu|5X=`LECNPa1XzIohL4wKnJ;r;>C3h8LlwC$iS|daD8T@L5`5=O!&Y)yRy{xz}A<`ajO>ousAJdh%8!CqO4J!%*Mb z@O!Ck4;pdgZq`DQc{*oNybvyczF$2yqAc;#ODU~yrDrj%#Fp^3Lpm0g;)+2dY-dPkwDP(2fBUR~(f|Me delta 766 zcmVDb0b7^mGlQ#r5fBrxCb^rhZI7vi7R7efYl)Fz9Q51)FQHUlgXaSvpC^XVh zLQh2}-Bws&<)1-AYhh#Ptd-8nMiRm!p}|H7quFdC35npE_+GznGKXAl7GpWT{(6Jr?uf52H(R^<)KK{y55 zL*ef?6^HOG8nV*y#x+I(`Zrd&fF`5USI~wylLht@h*ciJeh051*BTWaFi2<_qk!<| z*oTlx-^M5)`swE>9Dw$DgB@PLA}ql+ShoV|4t$0^e=@D}Z{Mk^{&DrcL)n5yU_e)L z0YdtAoUx2)Y;&+Ke>gW*sZ8beY{5@>0B*wx>_V=WROs+NT!#bUIcF&H1HsA-u3b;7$IK> wCuTnS^T@n`+G?K6oIrVtYn!Esx!kqZUq|$!f?nC_Pyhe`07*qoM6N<$f;yU2RR910 diff --git a/public/images/logos/csv.png b/public/images/logos/csv.png new file mode 100644 index 0000000000000000000000000000000000000000..6b717761541d39bac7ecc7f280498896a2ddfa6b GIT binary patch literal 3515 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 literal 0 HcmV?d00001 diff --git a/public/images/logos/plaid.png b/public/images/logos/plaid.png new file mode 100644 index 0000000000000000000000000000000000000000..23187ce8aa1f2923bd3517e1416ecfb6af29df96 GIT binary patch literal 8326 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 J{331~_&+?Cnk@hT literal 0 HcmV?d00001 diff --git a/public/images/logos/spectre.png b/public/images/logos/spectre.png new file mode 100644 index 0000000000000000000000000000000000000000..6c2189e96c34f3a4263953d0801a93fc4792eccf GIT binary patch literal 3446 zcmZ{nc|6qH8^=eCHG~MG8e65zW;ZmJvF`>MG?_6CP0V6uFv(=OQlca-8M0J#Q4&ga zBGNiinj%`2N|d;EzoG8$`uW}aeZ9{2e9!ZI-p@JDbI$qWo9y9cFD<}39UW{i zg5G?&iHi!}$oc0CK_|q-*jocC9;%H91`>1!KPCVmrLf$D0Ngxf06@5e?2Tn%U0skw z8U;$&PYWVJ*%Z1!Er`TM3O*?$76Htrgi)DD_HK<81yb<6Y=&uoS0t>^-5OX|4=|d> zAb|~`22i*LN)ikPn=|$YBQZ91zi`25x5fb$i;jfBqN1XpQTk9CBLs#pH8q97^ z5P<^1JVa#?*bpjH^JkF1;@FUwL8LrT128qC;F}!KCFq8+GO$wtd55f9SSX`CHY--ay+KI|cJ!}1h?3+EH^c~TG1<|^ z%KHHDHkX#F(52WK61+4W6Q$;zo=~Qy^I)bzWaAT~tscck9Ht+OdINU?{S&y;i+l+O zad}%O2a7!a^fC?WbxxTnoLEa6b>GsRnI&8OlYb}_%e$iDoEUDmJXN`Q(K;bRGgCt@ zLjnDtuX9k5&kj#H?Rxa}VPnovvoUS3(NNtL8B|*RlBY<)L`5_ZA_@}cIVTYH~_@vaF1y`FqZ6umRVcR~vCo zYmz_z#ysU96fRRP0=aj!Esu(1O~m7P>x;ZDzz*+m7gr1r{*tukNnpGtNHYF*#_>L( zO+}?zMP~hP$TjNT2O)Gh{|!OYq~voZaze4M%ubVcR|aDK*a`ymJ6zaoFKnRAIBj<& ztT)50OF083P90RwObDMgYb3MT{D9rDoVf~ILUGv(;gb{1KuckF)g$AJbH4U=U1@Ti zTjWbfXEabmc^6Ft#CrxGJT_Pd79M}mnN<88NN31Owf~tJRsZc`UBKKF$Mz4l!bc;A z(ViYV2&n02_U0zR3dQ+o_A&(+WF=%MVwr$bMw{|knl_t9Gt=RCgmL*BV?aa#gAZ8IqDgr=!$^M?T*QR{N zVnSK0-+QGt;uppTd{vYT1B{4gkv3f9dtN;B?SAn!N$FrCB_T=F-yo!}k4UjuL zZ?v9Qn#|;?8xT&FFtZSy6DbP1^5LI+Hua^Y)go>^yd_!LGg^3p3&d7U3@=W{mi5on zQN@+bs*0qg3T+`ZWmK3|Y`zPW_ zx7OT}x)bkXlTODEeDhvo%ii=+rJ^yEfnL?@K|L}n2XKXaDaXH@eCJSGv^`iGW1@z& ziyTNbE_r_DzDJy=vNh9r$jYQesIhw7%vq%1Xhd|DYkyciVQK-L>OJmq(;>isD0f}^p*BgWPcr9@ptuHK**Dd8z1G!tA ztz~AUvDCAP!IIuD>pvZX$m@{av^x3eX3;pDU1*GeyW_3u?kcl+V%#fosoeFgGO1NusKCY z6bZjJS9ewKtJjRiLqD-B6jD!vSa~h4@N0MXfy0rD5q&+^9=Yj?JkdS+?X6IHo_iSJz5vz}vozS2E7 zHHPalcx8)CfYBU9qZ`*ZKQGew?Nis_)f|^P(%_x-Q%m*x?3j+Q(WYZa!?uIb)pf>T zXwuD`KaW(!!iBd!+((q}ZybO6#j7@ut?toz&5hPfeTF;6aSZph|k9OwCM>szp@a?XQio#E{T&u_TuYftUG)*+^= zinCYY@K8E9`?HNX-S1-DcPgr~Oj>WK!15BWkC_Hklbp6`m3F%Gc_-=vZghxPw@;15 zW?~y=f)>v=9l7NcUk)6u-a(SJ6xA~L*cngZ4>)2i3Zg=@tk(guk`Lv4k|HfKB#H<+ zHATkfkWZ8IH={iqRQIyRG7xhFwDYI2b{>n^Ds$OiA|+Saim?~_?R`~>5zC;y-{U;& zb@>jPS%JAr82T$hTPx_d;LoiLqU^hdMfxLkzdX8K%f7M@ zwAW&9G;h4^bxfF+2@8xro}bzLHJOC3Gv;e($NXS@$FlFm_hs{HI_gX1)sP#;aelj9 z`ZeZu4(oCeK>ZtrpqAQT=bcw30^l^Bfu)!4!A)rQ@i%}6mc5$I56m{eZ@#o2K-4Z+ zNat18&(v(!uMqa*G>AY97Mfm|p5&g&JC=GUs;nOpwOL%b3gY&LpHytnQK55WA4zN-R3ZuwA==`d7r-@lNImII(ud!b zcNmQJ-i7TuT0AB*U;F+&tppS5JefTwt>&z$dp6<}=*8fDp2tV-l`X8{)_BvKwVUG9 zXKi^DS45AE;&Z)6a&qkc*gq!bU@9a-Ku%A^@#F7==*_C{6KuJ z=sKClppbW}VIu7EqFmASLfy2ZRJBB$x3gfBx(D&QvWY}&nAsDjk0tB+dFX}hVFx6d zDcA9C#+&)^eUs=7jpFN;EDjF$$m2ms1Y6CJ5}Nb$$Xox?Y@Qw-6b(PJS68=L^0}Nl o;k8p!V#&Ri<9z{~2cLc{C0#i;dq?|Q@$%1*qph1wg*86mKe6n 'Prerequisites for an import from bunq', - 'bunq_prerequisites_text' => 'In order to import from bunq, you need to obtain an API key. You can do this through the app.', + 'bunq_prerequisites_title' => 'Prerequisites for an import from bunq', + 'bunq_prerequisites_text' => 'In order to import from bunq, you need to obtain an API key. You can do this through the app.', + + // Spectre: + 'spectre_title' => 'Import using Spectre', + 'spectre_prerequisites_title' => 'Prerequisites for an import using Spectre', + 'spectre_prerequisites_text' => 'In order to import data using the Spectre API, you need to prove some secrets. They can be found on the secrets page.', + 'spectre_enter_pub_key' => 'The import will only work when you enter this public key on your security page.', ]; diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index 6d7a98c2fe..9e3af6052c 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -186,6 +186,10 @@ return [ 'csv_delimiter' => 'CSV field delimiter', 'csv_import_account' => 'Default import account', 'csv_config' => 'CSV import configuration', + 'client_id' => 'Client ID', + 'service_secret' => 'Service secret', + 'app_secret' => 'App secret', + 'public_key' => 'Public key', 'due_date' => 'Due date', diff --git a/resources/views/import/index.twig b/resources/views/import/index.twig index 8e2876dc52..bb941e7c1c 100644 --- a/resources/views/import/index.twig +++ b/resources/views/import/index.twig @@ -20,21 +20,34 @@
-
-

+

+
{# bunq import #} - {# - + bunq
Import from bunq
- #} -

+
+
+ {# import from Spectre #} + + Spectre
+ Import using Spectre +
+
+
+ {# import from Plaid #} + + Plaid
+ Import using Plaid +
+
diff --git a/resources/views/import/spectre/prerequisites.twig b/resources/views/import/spectre/prerequisites.twig new file mode 100644 index 0000000000..1b225e6a12 --- /dev/null +++ b/resources/views/import/spectre/prerequisites.twig @@ -0,0 +1,58 @@ +{% extends "./layout/default" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists }} +{% endblock %} +{% block content %} +
+
+ +
+
+
+

{{ trans('bank.spectre_prerequisites_title') }}

+
+
+
+
+

+ {{ trans('bank.spectre_prerequisites_text')|raw }} +

+
+
+ +
+
+ {{ ExpandedForm.text('client_id') }} + {{ ExpandedForm.text('service_secret') }} + {{ ExpandedForm.text('app_secret') }} +
+
+
+
+

{{ trans('bank.spectre_enter_pub_key')|raw }}

+
+ + +
+ +
+
+
+
+ +
+
+ +
+{% endblock %} +{% block scripts %} +{% endblock %} +{% block styles %} +{% endblock %}