Merge pull request #9584 from beganovich/1403-gocardless

GoCardless: OAuth
This commit is contained in:
David Bomba 2024-09-25 06:29:18 +10:00 committed by GitHub
commit 47a7d97e4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 371 additions and 1 deletions

View File

@ -65,4 +65,7 @@ APPLE_REDIRECT_URI=
NORDIGEN_SECRET_ID=
NORDIGEN_SECRET_KEY=
GOCARDLESS_CLIENT_ID=
GOCARDLESS_CLIENT_SECRET=
OPENEXCHANGE_APP_ID=

View File

@ -0,0 +1,109 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Gateways;
use App\DataMapper\FeesAndLimits;
use App\Factory\CompanyGatewayFactory;
use App\Http\Controllers\Controller;
use App\Http\Requests\GoCardless\OAuthConnectConfirmRequest;
use App\Http\Requests\GoCardless\OAuthConnectRequest;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use Illuminate\Support\Facades\Http;
class GoCardlessOAuthController extends Controller
{
public function connect(OAuthConnectRequest $request): \Illuminate\Http\RedirectResponse
{
/** @var \App\Models\Company $company */
$company = $request->getCompany();
$params = [
'client_id' => config('services.gocardless.client_id'),
'redirect_uri' => config('services.gocardless.redirect_uri'),
'scope' => 'read_write',
'response_type' => 'code',
'state' => $company->company_key,
'prefill[email]' => $company->settings->email,
'prefill[organisation_name]' => $company->settings->name,
'prefill[country_code]' => $company->country()->iso_3166_2,
];
$url = config('services.gocardless.environment') === 'production'
? 'https://connect.gocardless.com/oauth/authorize?%s'
: 'https://connect-sandbox.gocardless.com/oauth/authorize?%s';
return redirect()->to(
sprintf($url, http_build_query($params))
);
}
public function confirm(OAuthConnectConfirmRequest $request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View
{
/** @var \App\Models\Company $company */
$company = $request->getCompany();
// LBo0v_561xgFGnFUae6uEQEfrWoSEMnZ&state=5O2O85C8dPv1Gp1UPVq0xs4FVTZdq5dO
// https://invoicing.co/gocardless/oauth/connect/confirm?code=sH55_xb-2s1JtuEw-j7W0hT0Z1sFkM7l
$url = config('services.gocardless.environment') === 'production'
? 'https://connect.gocardless.com/oauth/access_token'
: 'https://connect-sandbox.gocardless.com/oauth/access_token';
$response = Http::post($url, [
'client_id' => config('services.gocardless.client_id'),
'client_secret' => config('services.gocardless.client_secret'),
'grant_type' => 'authorization_code',
'code' => $request->query('code'),
'redirect_uri' => config('services.gocardless.redirect_uri'),
]);
if ($response->failed()) {
return view('auth.gocardless_connect.access_denied');
}
$company_gateway = CompanyGateway::query()
->where('gateway_key', 'b9886f9257f0c6ee7c302f1c74475f6c')
->where('company_id', $company->id)
->first();
if ($company_gateway === null) {
$company_gateway = CompanyGatewayFactory::create($company->id, $company->owner()->id);
$fees_and_limits = new \stdClass();
$fees_and_limits->{GatewayType::INSTANT_BANK_PAY} = new FeesAndLimits();
$company_gateway->gateway_key = 'b9886f9257f0c6ee7c302f1c74475f6c';
$company_gateway->fees_and_limits = $fees_and_limits;
$company_gateway->setConfig([]);
}
$response = $response->json();
$payload = [
'__current' => $company_gateway->getConfig(),
'account_id' => $response['organisation_id'],
'token_type' => $response['token_type'],
'scope' => $response['scope'],
'active' => $response['active'],
'accessToken' => $response['access_token'],
'testMode' => $company_gateway->getConfigField('testMode'),
'oauth2' => true,
];
$company_gateway->setConfig($payload);
$company_gateway->save();
return view('auth.gocardless_connect.completed');
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Gateways;
use App\Http\Controllers\Controller;
use App\Http\Requests\GoCardless\WebhookRequest;
use App\Models\CompanyGateway;
use App\Repositories\CompanyRepository;
use Illuminate\Support\Arr;
class GoCardlessOAuthWebhookController extends Controller
{
public function __construct(
protected CompanyRepository $company_repository,
) {
}
public function __invoke(WebhookRequest $request)
{
foreach ($request->events as $event) {
nlog($event['action']);
$e = Arr::dot($event);
if ($event['action'] === 'disconnected') {
/** @var \App\Models\CompanyGateway $company_gateway */
$company_gateway = CompanyGateway::query()
->whereJsonContains('config->account_id', $e['links.organisation'])
->firstOrFail();
$current = $company_gateway->getConfig('__current');
if ($current) {
$company_gateway->setConfig($current);
$company_gateway->save();
}
$this->company_repository->archive($company_gateway);
}
}
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\GoCardless;
use App\Libraries\MultiDB;
use App\Models\Company;
use Illuminate\Foundation\Http\FormRequest;
class OAuthConnectConfirmRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'state' => ['required', 'string'],
'code' => ['required','string'],
];
}
public function getCompany(): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder|\App\Models\BaseModel
{
MultiDB::findAndSetDbByCompanyKey(
$this->query('state'),
);
return Company::query()
->where('company_key', $this->query('state'))
->firstOrFail();
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\GoCardless;
use App\Libraries\MultiDB;
use App\Models\Company;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Cache;
class OAuthConnectRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
//
];
}
public function getCompany(): \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Builder|\App\Models\BaseModel
{
$data = Cache::get(
key: $this->token,
);
MultiDB::findAndSetDbByCompanyKey(
company_key: $data['company_key'],
);
return Company::query()
->where('company_key', $data['company_key'])
->firstOrFail();
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\GoCardless;
use Illuminate\Foundation\Http\FormRequest;
class WebhookRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'events' => ['required', 'array'],
'events.*.resource_type' => ['required', 'string'],
];
}
}

View File

@ -131,6 +131,12 @@ return [
'client_id' => env('CHORUS_CLIENT_ID', false),
'secret' => env('CHORUS_SECRET', false),
],
'gocardless' => [
'client_id' => env('GOCARDLESS_CLIENT_ID', null),
'client_secret' => env('GOCARDLESS_CLIENT_SECRET', null),
'environment' => env('GOCARDLESS_ENVIRONMENT', 'production'),
'redirect_uri' => env('GOCARDLESS_REDIRECT_URI', 'https://invoicing.co/gocardless/oauth/connect/confirm'),
],
'quickbooks' => [
'client_id' => env('QUICKBOOKS_CLIENT_ID', false),
'client_secret' => env('QUICKBOOKS_CLIENT_SECRET', false),

View File

@ -0,0 +1,30 @@
@extends('layouts.ninja')
@section('meta_title', ctrans('texts.error_title'))
@section('body')
<div class="flex flex-col justify-center items-center mt-10">
<div class="mb-4">
<svg height="60" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0 {
fill: #F1F252;
}
.st1 {
fill: #1C1B18;
}
</style>
<circle class="st0" cx="500" cy="500" r="500" />
<path class="st1" d="M507.9,242.1c55.2,0,86.2,9,86.2,9l91.7,187.2l-0.8,0.8l-118-70.4c-68.4-40.7-118-62.1-158.4-60.5
c-42.7,0.8-68.3,35.2-68.3,85c1.5,127.5,122.7,284.5,243.1,284.5c49.1,0,74.7-15.8,89.7-34.9L494.8,447.3v-0.8h244.8
c3.3,17.5,5.2,35.3,5.4,53.1c0,143.1-109.5,258.3-244.6,258.3C364.7,757.9,255,642.7,255,499.6C254.8,357.3,364.3,242.1,507.9,242.1
z" />
</svg>
</div>
<p>We were unable to connect to GoCardless as access was denied.</p>
<span>Click <a class="font-semibold hover:underline" href="{{ url('/#/settings/company_gateways') }}">here</a> to
continue.</span>
</div>
@endsection

View File

@ -0,0 +1,31 @@
@extends('layouts.ninja')
@section('meta_title', ctrans('texts.success'))
@section('body')
<div class="flex flex-col justify-center items-center mt-10">
<div class="mb-4">
<svg height="60" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000"
style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0 {
fill: #F1F252;
}
.st1 {
fill: #1C1B18;
}
</style>
<circle class="st0" cx="500" cy="500" r="500" />
<path class="st1" d="M507.9,242.1c55.2,0,86.2,9,86.2,9l91.7,187.2l-0.8,0.8l-118-70.4c-68.4-40.7-118-62.1-158.4-60.5
c-42.7,0.8-68.3,35.2-68.3,85c1.5,127.5,122.7,284.5,243.1,284.5c49.1,0,74.7-15.8,89.7-34.9L494.8,447.3v-0.8h244.8
c3.3,17.5,5.2,35.3,5.4,53.1c0,143.1-109.5,258.3-244.6,258.3C364.7,757.9,255,642.7,255,499.6C254.8,357.3,364.3,242.1,507.9,242.1
z" />
</svg>
</div>
<p>Connecting your account using GoCardless has been successfully completed.</p>
<span>Click <a class="font-semibold hover:underline" href="{{ url('/#/settings/company_gateways') }}">here</a> to
continue.</span>
</div>
@endsection

View File

@ -10,6 +10,8 @@ use App\Http\Controllers\ClientPortal\ApplePayDomainController;
use App\Http\Controllers\EInvoice\SelfhostController;
use App\Http\Controllers\Gateways\Checkout3dsController;
use App\Http\Controllers\Gateways\GoCardlessController;
use App\Http\Controllers\Gateways\GoCardlessOAuthController;
use App\Http\Controllers\Gateways\GoCardlessOAuthWebhookController;
use App\Http\Controllers\Gateways\Mollie3dsController;
use App\Http\Controllers\SetupController;
use App\Http\Controllers\StripeConnectController;
@ -51,6 +53,10 @@ Route::get('mollie/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', [Mol
Route::get('gocardless/ibp_redirect/{company_key}/{company_gateway_id}/{hash}', [GoCardlessController::class, 'ibpRedirect'])->middleware('domain_db')->name('gocardless.ibp_redirect');
Route::get('.well-known/apple-developer-merchantid-domain-association', [ApplePayDomainController::class, 'showAppleMerchantId']);
Route::get('gocardless/oauth/connect/confirm', [GoCardlessOAuthController::class, 'confirm'])->name('gocardless.oauth.confirm');
Route::post('gocardless/oauth/connect/webhook', GoCardlessOAuthWebhookController::class)->name('gocardless.oauth.webhook');
Route::get('gocardless/oauth/connect/{token}', [GoCardlessOAuthController::class, 'connect']);
Route::get('einvoice/beta', [SelfhostController::class, 'index'])->name('einvoice.beta');
\Illuminate\Support\Facades\Broadcast::routes(['middleware' => ['token_auth']]);