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 c1223bc4fc..fad92342b7 100644 Binary files a/public/images/logos/bunq.png and b/public/images/logos/bunq.png differ diff --git a/public/images/logos/csv.png b/public/images/logos/csv.png new file mode 100644 index 0000000000..6b71776154 Binary files /dev/null and b/public/images/logos/csv.png differ diff --git a/public/images/logos/plaid.png b/public/images/logos/plaid.png new file mode 100644 index 0000000000..23187ce8aa Binary files /dev/null and b/public/images/logos/plaid.png differ diff --git a/public/images/logos/spectre.png b/public/images/logos/spectre.png new file mode 100644 index 0000000000..6c2189e96c Binary files /dev/null and b/public/images/logos/spectre.png differ diff --git a/resources/lang/en_US/bank.php b/resources/lang/en_US/bank.php index 4becc5984c..369f72e145 100644 --- a/resources/lang/en_US/bank.php +++ b/resources/lang/en_US/bank.php @@ -3,6 +3,12 @@ declare(strict_types=1); return [ - '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.', + '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 @@
+