Initial integration with Quicken (Intuit API), rework oauth tables, update/test google login

This commit is contained in:
Deon George 2022-08-12 14:53:06 +10:00
parent 70571cb6ac
commit 8fd79ce23e
16 changed files with 299 additions and 99 deletions

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -10,9 +11,7 @@ use Laravel\Socialite\Facades\Socialite;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Mail\SocialLink; use App\Mail\SocialLink;
use App\Models\Oauth; use App\Models\{ProviderOauth,ProviderToken,User,UserOauth};
use App\Models\AccountOauth;
use App\Models\User;
use App\Providers\RouteServiceProvider; use App\Providers\RouteServiceProvider;
class SocialLoginController extends Controller class SocialLoginController extends Controller
@ -26,10 +25,13 @@ class SocialLoginController extends Controller
{ {
$openiduser = Socialite::with($provider)->user(); $openiduser = Socialite::with($provider)->user();
$oo = Oauth::firstOrCreate(['name'=>$provider,'active'=>TRUE]); if (! $openiduser)
return redirect('/home')->with('error','No user details obtained.');
$oo = ProviderOauth::firstOrCreate(['name'=>$provider,'active'=>TRUE]);
// See if this user has connected and linked previously // See if this user has connected and linked previously
$aoo = $oo->accounts->where('userid',$openiduser->id); $aoo = $oo->users->where('userid',$openiduser->id);
if ($aoo->count() == 1) { if ($aoo->count() == 1) {
$aoo = $aoo->first(); $aoo = $aoo->first();
@ -59,10 +61,10 @@ class SocialLoginController extends Controller
// See if their is an account with this email address // See if their is an account with this email address
if ($uo->count() == 1) { if ($uo->count() == 1) {
$aoo = new AccountOauth; $aoo = new UserOauth;
$aoo->userid = $openiduser->id; $aoo->userid = $openiduser->id;
$aoo->oauth_data = $openiduser->user; $aoo->oauth_data = $openiduser->user;
$oo->accounts()->save($aoo); $oo->users()->save($aoo);
return $this->link($provider,$aoo,$uo->first()); return $this->link($provider,$aoo,$uo->first());
@ -78,16 +80,43 @@ class SocialLoginController extends Controller
return redirect()->intended(RouteServiceProvider::HOME); return redirect()->intended(RouteServiceProvider::HOME);
} }
public function handleBearerTokenCallback($provider)
{
$openiduser = Socialite::with($provider)->user();
if (! $openiduser)
return redirect('/home')->with('error','No user details obtained.');
$po = ProviderOauth::where('name',$provider)->singleOrFail();
$uoo = ProviderToken::where('user_id',Auth::id())->where('provider_oauth_id',$po->id)->firstOrNew();
$uoo->user_id = Auth::id();
$uoo->access_token = $openiduser->token;
$uoo->access_token_expires_at = Carbon::now()->addSeconds($openiduser->expiresIn);
$uoo->refresh_token = $openiduser->refreshToken;
$uoo->refresh_token_expires_at = Carbon::now()->addSeconds($openiduser->refresh_token_expires_in);
$uoo->realm_id = $openiduser->realmid;
$po->tokens()->save($uoo);
return redirect()
->intended(RouteServiceProvider::HOME)
->with('success','Token refreshed.');
}
/** /**
* We have identified the user and oauth, just need them to confirm the link * We have identified the user and oauth, just need them to confirm the link
* *
* @param $provider * @param $provider
* @param UserOauth $ao
* @param User $uo * @param User $uo
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View * @return \Illuminate\View\View
*/ */
public function link($provider,AccountOauth $ao,User $uo) public function link($provider,UserOauth $ao,User $uo): \Illuminate\View\View
{ {
Mail::to($uo->email)->send(new SocialLink($ao)); // @note If this is sent now (send()), it results in the caller to be executed a second time (handleProviderCallback()).
Mail::to($uo->email)->queue(new SocialLink($ao));
return view('auth.social_link') return view('auth.social_link')
->with('oauthid',$ao->id) ->with('oauthid',$ao->id)
@ -97,7 +126,7 @@ class SocialLoginController extends Controller
public function linkcomplete(Request $request,$provider) public function linkcomplete(Request $request,$provider)
{ {
// Load our oauth id // Load our oauth id
$aoo = AccountOauth::findOrFail($request->post('oauthid')); $aoo = UserOauth::findOrFail($request->post('oauthid'));
// Check our email matches // Check our email matches
if (Arr::get($aoo->oauth_data,'email','invalid') !== $request->post('email')) if (Arr::get($aoo->oauth_data,'email','invalid') !== $request->post('email'))

View File

@ -6,24 +6,26 @@ use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use App\Models\{AccountOauth,User}; use App\Models\{Site,User,UserOauth};
class SocialLink extends Mailable class SocialLink extends Mailable
{ {
use Queueable, SerializesModels; use Queueable, SerializesModels;
public string $token; public string $token;
public User $user; public Site $site;
public ?User $user;
/** /**
* Create a new message instance. * Create a new message instance.
* *
* @param AccountOauth $o * @param UserOauth $o
*/ */
public function __construct(AccountOauth $o) public function __construct(UserOauth $o)
{ {
$this->site = $o->site;
$this->token = $o->link_token; $this->token = $o->link_token;
$this->user = $o->account->user; $this->user = $o->user;
} }
/** /**
@ -37,7 +39,7 @@ class SocialLink extends Mailable
->markdown('email.system.social_link') ->markdown('email.system.social_link')
->subject('Link your Account') ->subject('Link your Account')
->with([ ->with([
'site'=>$this->user->site, 'site'=>$this->site,
]); ]);
} }
} }

View File

@ -1,46 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\NextKey;
class AccountOauth extends Model
{
use NextKey;
const RECORD_ID = 'account_oauth';
public $incrementing = FALSE;
protected $table = 'ab_account_oauth';
const CREATED_AT = 'date_orig';
const UPDATED_AT = 'date_last';
public $dateFormat = 'U';
protected $casts = [
'oauth_data'=>'array',
];
public function account()
{
return $this->belongsTo(Account::class);
}
public function site()
{
return $this->belongsTo(Site::class);
}
public function User()
{
return $this->belongsTo(User::class);
}
/**
* Get a link token to use when validating account.
*/
public function getLinkTokenAttribute(): string
{
return strtoupper(substr(md5($this->id.$this->date_last),0,8));
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\NextKey;
class Oauth extends Model
{
use NextKey;
const RECORD_ID = 'oauth';
public $incrementing = FALSE;
protected $table = 'ab_oauth';
public $timestamps = FALSE;
protected $fillable = ['name','active'];
public function accounts()
{
return $this->hasMany(AccountOauth::class);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ProviderOauth extends Model
{
protected $table = 'provider_oauth';
protected $fillable = ['name','active'];
/* RELATIONS */
public function tokens()
{
return $this->hasMany(ProviderToken::class);
}
public function users()
{
return $this->hasMany(UserOauth::class);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\SiteID;
class ProviderToken extends Model
{
use SiteID;
protected $dates = [
'access_token_expires_at',
'refresh_token_expires_at',
];
}

31
app/Models/UserOauth.php Normal file
View File

@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\SiteID;
class UserOauth extends Model
{
use SiteID;
protected $table = 'user_oauth';
protected $casts = [
'oauth_data'=>'json',
];
public function User()
{
return $this->belongsTo(User::class);
}
/**
* Get a link token to use when validating account.
*/
public function getLinkTokenAttribute(): string
{
return strtoupper(substr(md5($this->id.$this->date_last),0,8));
}
}

View File

@ -3,6 +3,7 @@
namespace App\Providers; namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;
use Leenooks\Traits\SingleOrFail; use Leenooks\Traits\SingleOrFail;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -16,7 +17,7 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function register() public function register()
{ {
// Passport::ignoreMigrations();
} }
/** /**

View File

@ -2,12 +2,15 @@
namespace App\Providers; namespace App\Providers;
use Intuit\Traits\IntuitSocialite;
use Laravel\Passport\Passport; use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider
{ {
use IntuitSocialite;
/** /**
* The policy mappings for the application. * The policy mappings for the application.
* *
@ -25,6 +28,7 @@ class AuthServiceProvider extends ServiceProvider
public function boot() public function boot()
{ {
$this->registerPolicies(); $this->registerPolicies();
$this->bootIntuitSocialite();
Passport::routes(); Passport::routes();
// Passport::enableImplicitGrant(); // Passport::enableImplicitGrant();

View File

@ -21,6 +21,7 @@
"laravel/socialite": "^5.2", "laravel/socialite": "^5.2",
"laravel/ui": "^3.2", "laravel/ui": "^3.2",
"leenooks/dreamscape": "^0.1.0", "leenooks/dreamscape": "^0.1.0",
"leenooks/intuit": "^0.1.0",
"leenooks/laravel": "^9.2.3", "leenooks/laravel": "^9.2.3",
"leenooks/laravel-theme": "^v2.0.18", "leenooks/laravel-theme": "^v2.0.18",
"nunomaduro/laravel-console-summary": "^1.8", "nunomaduro/laravel-console-summary": "^1.8",
@ -50,6 +51,10 @@
} }
}, },
"repositories": { "repositories": {
"intuit": {
"type": "vcs",
"url": "https://dev.leenooks.net/leenooks/intuit"
},
"laravel": { "laravel": {
"type": "vcs", "type": "vcs",
"url": "https://dev.leenooks.net/leenooks/laravel" "url": "https://dev.leenooks.net/leenooks/laravel"

52
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "d3d12d0317a55fce7dedf86c4c567aba", "content-hash": "d82522975cd8f5a5e902d49a94527919",
"packages": [ "packages": [
{ {
"name": "asm89/stack-cors", "name": "asm89/stack-cors",
@ -142,16 +142,16 @@
}, },
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.10.1", "version": "0.10.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/brick/math.git", "url": "https://github.com/brick/math.git",
"reference": "de846578401f4e58f911b3afeb62ced56365ed87" "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/de846578401f4e58f911b3afeb62ced56365ed87", "url": "https://api.github.com/repos/brick/math/zipball/459f2781e1a08d52ee56b0b1444086e038561e3f",
"reference": "de846578401f4e58f911b3afeb62ced56365ed87", "reference": "459f2781e1a08d52ee56b0b1444086e038561e3f",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -186,7 +186,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/brick/math/issues", "issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.10.1" "source": "https://github.com/brick/math/tree/0.10.2"
}, },
"funding": [ "funding": [
{ {
@ -194,7 +194,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-08-01T22:54:31+00:00" "time": "2022-08-10T22:54:19+00:00"
}, },
{ {
"name": "clarkeash/doorman", "name": "clarkeash/doorman",
@ -3371,6 +3371,44 @@
], ],
"time": "2022-08-10T06:07:35+00:00" "time": "2022-08-10T06:07:35+00:00"
}, },
{
"name": "leenooks/intuit",
"version": "0.1.0",
"source": {
"type": "git",
"url": "https://dev.leenooks.net/leenooks/intuit",
"reference": "bbbdff040fa7d49e5acec38ce3a7c5a35af58a30"
},
"require": {
"jenssegers/model": "^1.5"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Intuit\\Providers\\IntuitServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Intuit\\": "src"
}
},
"authors": [
{
"name": "Deon George",
"email": "deon@leenooks.net"
}
],
"description": "Intuit API",
"keywords": [
"intuit",
"laravel",
"leenooks"
],
"time": "2022-08-12T04:41:22+00:00"
},
{ {
"name": "leenooks/laravel", "name": "leenooks/laravel",
"version": "9.2.5", "version": "9.2.5",

View File

@ -194,6 +194,6 @@ return [
| |
*/ */
'same_site' => 'strict', 'same_site' => 'lax',
]; ];

View File

@ -0,0 +1,111 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('oauth_access_tokens');
Schema::dropIfExists('oauth_auth_codes');
Schema::dropIfExists('oauth_clients');
Schema::dropIfExists('oauth_personal_access_clients');
Schema::dropIfExists('oauth_refresh_tokens');
Schema::dropIfExists('external_integrations');
Schema::dropIfExists('external_account');
DB::statement('RENAME TABLE ab_oauth TO provider_oauth');
DB::statement('ALTER TABLE provider_oauth MODIFY active tinyint(1) NOT NULL');
DB::statement('ALTER TABLE provider_oauth MODIFY app_id VARCHAR(256) NOT NULL,MODIFY secret VARCHAR(256) DEFAULT NULL');
Schema::table('provider_oauth', function (Blueprint $table) {
$table->dropForeign('ab_oauth_site_id_foreign');
$table->dropIndex('ab_oauth_id_site_id_index');
$table->dropIndex('ab_oauth_site_id_foreign');
$table->foreign(['site_id'])->references(['id'])->on('sites');
});
DB::statement('RENAME TABLE quickbooks_tokens TO provider_tokens');
DB::statement('ALTER TABLE provider_tokens MODIFY realm_id TEXT DEFAULT NULL,MODIFY access_token TEXT NOT NULL,MODIFY refresh_token TEXT DEFAULT NULL');
DB::statement('ALTER TABLE provider_tokens MODIFY access_token_expires_at datetime DEFAULT NULL,MODIFY refresh_token_expires_at datetime DEFAULT NULL');
Schema::table('provider_tokens', function (Blueprint $table) {
$table->integer('provider_oauth_id')->unsigned();
$table->integer('site_id')->unsigned();
$table->unique(['provider_oauth_id','user_id']);
$table->foreign(['user_id','site_id'])->references(['id','site_id'])->on('users');
$table->foreign(['provider_oauth_id','site_id'])->references(['id','site_id'])->on('provider_oauth');
});
// delete from ab_account_oauth where account_id=144;
// update ab_account_oauth set user_id =1 where account_id=1;
// update ab_account_oauth set user_id =109 where id=12;
DB::statement('RENAME TABLE ab_account_oauth TO user_oauth');
DB::statement('ALTER TABLE user_oauth RENAME COLUMN oauth_id TO provider_oauth_id');
Schema::table('user_oauth', function (Blueprint $table) {
$table->datetime('created_at')->nullable()->after('id');
$table->datetime('updated_at')->nullable()->after('created_at');
});
foreach (\App\Models\UserOauth::withoutGlobalScope(\App\Models\Scopes\SiteScope::class)->cursor() as $o) {
if ($o->date_orig)
$o->created_at = \Carbon\Carbon::createFromTimestamp($o->date_orig);
if ($o->date_last)
$o->updated_at = \Carbon\Carbon::createFromTimestamp($o->date_last);
if ($o->getRawOriginal('oauth_data') && ! is_array($o->oauth_data)) {
try {
$o->oauth_data = unserialize(gzuncompress($o->getRawOriginal('oauth_data')));
} catch (Exception $e) {
}
}
$o->save();
}
DB::statement('ALTER TABLE user_oauth MODIFY provider_oauth_id int unsigned NOT NULL');
Schema::table('user_oauth', function (Blueprint $table) {
$table->dropColumn(['date_orig','date_last','account_id']);
$table->dropForeign('ab_account_oauth_site_id_foreign');
$table->dropIndex('ab_account_oauth_site_id_foreign');
$table->dropIndex('ab_account_oauth_id_site_id_index');
$table->foreign(['user_id','site_id'])->references(['id','site_id'])->on('users');
$table->foreign(['provider_oauth_id','site_id'])->references(['id','site_id'])->on('provider_oauth');
});
// Fix incorrect site references
Schema::table('accounts', function (Blueprint $table) {
$table->dropForeign(['site_id']);
});
Schema::table('services', function (Blueprint $table) {
$table->dropForeign(['site_id']);
});
Schema::table('costs', function (Blueprint $table) {
$table->dropForeign(['site_id']);
$table->foreign(['site_id'])->references(['id'])->on('sites');
});
Schema::table('products', function (Blueprint $table) {
$table->dropForeign(['site_id']);
$table->foreign(['site_id'])->references(['id'])->on('sites');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
abort(500,'cant go back');
}
};

View File

@ -71,6 +71,12 @@
@can('wholesaler') @can('wholesaler')
<li class="nav-header">ADMIN</li> <li class="nav-header">ADMIN</li>
<li class="nav-item">
<a href="{{ url('auth/intuit') }}" class="nav-link @if(preg_match('#^auth/intuit#',$path)) active @endif">
<i class="nav-icon fas fa-calculator"></i> <p>Quickbooks Link</p>
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a href="{{ url('a/setup') }}" class="nav-link @if(preg_match('#^a/setup#',$path)) active @endif"> <a href="{{ url('a/setup') }}" class="nav-link @if(preg_match('#^a/setup#',$path)) active @endif">
<i class="nav-icon fas fa-cogs"></i> <p>Site Setup</p> <i class="nav-icon fas fa-cogs"></i> <p>Site Setup</p>

View File

@ -46,9 +46,11 @@ Route::get('logout',[LoginController::class,'logout'])
Route::group(['middleware'=>['theme:adminlte-be']],function() { Route::group(['middleware'=>['theme:adminlte-be']],function() {
Route::get('auth/{socialProvider}',[SocialLoginController::class,'redirectToProvider']); Route::get('auth/{socialProvider}',[SocialLoginController::class,'redirectToProvider']);
Route::get('auth/{socialProvider}/callback',[SocialLoginController::class,'handleProviderCallback']); Route::get('auth/{socialProvider}/callback',[SocialLoginController::class,'handleProviderCallback']);
Route::get('auth/{socialProvider}/token',[SocialLoginController::class,'handleBearerTokenCallback']);
Route::get('auth/{socialProvider}/link',[SocialLoginController::class,'link']); Route::get('auth/{socialProvider}/link',[SocialLoginController::class,'link']);
Route::post('auth/{socialProvider}/linkcomplete',[SocialLoginController::class,'linkcomplete']); Route::post('auth/{socialProvider}/linkcomplete',[SocialLoginController::class,'linkcomplete']);
}); });
// Return from user switch // Return from user switch
Route::get('admin/switch/stop',[SwitchUserController::class,'switch_stop']) Route::get('admin/switch/stop',[SwitchUserController::class,'switch_stop'])
->middleware('auth') ->middleware('auth')