Working on client login routes

This commit is contained in:
David Bomba 2019-07-08 10:08:57 +10:00
parent 83f6a88cb3
commit 51b0c17c4c
15 changed files with 534 additions and 10 deletions

View File

@ -0,0 +1,308 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers\Contact;
use App\Http\Controllers\BaseController;
use App\Http\Controllers\Controller;
use App\Jobs\Account\CreateAccount;
use App\Libraries\MultiDB;
use App\Libraries\OAuth\OAuth;
use App\Models\ClientContact;
use App\Models\User;
use App\Transformers\ClientContactTransformer;
use App\Transformers\UserTransformer;
use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Laravel\Socialite\Facades\Socialite;
class LoginController extends BaseController
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
use UserSessionAttributes;
protected $entity_type = ClientContact::class;
protected $entity_transformer = ClientContactTransformer::class;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Once the user is authenticated, we need to set
* the default company into a session variable
*
* @return void
* deprecated .1 API ONLY we don't need to set any session variables
*/
public function authenticated(Request $request, User $user) : void
{
//$this->setCurrentCompanyId($user->companies()->first()->account->default_company_id);
}
/**
* Login via API
*
* @param \Illuminate\Http\Request $request The request
*
* @return Response|User Process user login.
*/
public function apiLogin(Request $request)
{
/*
if (auth()->guard('contact')->attempt(['email' => $request->email, 'password' => $request->password], false) {
return redirect()->intended('/admin');
}
*/
Auth::shouldUse('contact');
$this->validateLogin($request);
if ($this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
return response()->json(['message' => 'Too many login attempts, you are being throttled']);
}
if ($this->attemptLogin($request))
return $this->itemResponse($this->guard()->user());
else {
$this->incrementLoginAttempts($request);
return response()->json(['message' => ctrans('texts.invalid_credentials')]);
}
}
/**
* Redirect the user to the provider authentication page
*
* @return void
*/
public function redirectToProvider(string $provider)
{
//'https://www.googleapis.com/auth/gmail.send','email','profile','openid'
//
if(request()->has('code'))
return $this->handleProviderCallback($provider);
else
return Socialite::driver($provider)->scopes()->redirect();
}
public function redirectToProviderAndCreate(string $provider)
{
$redirect_url = config('services.' . $provider . '.redirect') . '/create';
if(request()->has('code'))
return $this->handleProviderCallbackAndCreate($provider);
else
return Socialite::driver($provider)->redirectUrl($redirect_url)->redirect();
}
/*
public function handleProviderCallbackAndCreate(string $provider)
{
$socialite_user = Socialite::driver($provider)
->stateless()
->user();
if($user = OAuth::handleAuth($socialite_user, $provider))
{
Auth::login($user, true);
return redirect($this->redirectTo);
}
else if(MultiDB::checkUserEmailExists($socialite_user->getEmail()))
{
Session::flash('error', 'User exists in system, but not with this authentication method'); //todo add translations
return view('auth.login');
}
else {
//todo
$name = OAuth::splitName($socialite_user->getName());
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $socialite_user->getEmail(),
'oauth_user_id' => $socialite_user->getId(),
'oauth_provider_id' => $provider
];
$account = CreateAccount::dispatchNow($new_account);
Auth::login($account->default_company->owner(), true);
$cookie = cookie('db', $account->default_company->db);
return redirect($this->redirectTo)->withCookie($cookie);
}
}
*/
/**
* We use this function when OAUTHING via the web interface
*
* @return redirect
public function handleProviderCallback(string $provider)
{
$socialite_user = Socialite::driver($provider)
->stateless()
->user();
if($user = OAuth::handleAuth($socialite_user, $provider))
{
Auth::login($user, true);
return redirect($this->redirectTo);
}
else if(MultiDB::checkUserEmailExists($socialite_user->getEmail()))
{
Session::flash('error', 'User exists in system, but not with this authentication method'); //todo add translations
return view('auth.login');
}
else {
//todo
$name = OAuth::splitName($socialite_user->getName());
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $socialite_user->getEmail(),
'oauth_user_id' => $socialite_user->getId(),
'oauth_provider_id' => $provider
];
$account = CreateAccount::dispatchNow($new_account);
Auth::login($account->default_company->owner(), true);
$cookie = cookie('db', $account->default_company->db);
return redirect($this->redirectTo)->withCookie($cookie);
}
}
*/
/**
* A client side authentication has taken place.
* We now digest the token and confirm authentication with
* the authentication server, the correct user object
* is returned to us here and we send back the correct
* user object payload - or error.
*
* This can be extended to a create route also - need to pass a ?create query parameter and
* then process the signup
*
* return User $user
*/
public function oauthApiLogin()
{
$user = false;
$oauth = new OAuth();
$user = $oauth->getProvider(request()->input('provider'))->getTokenResponse(request()->input('token'));
if ($user)
return $this->itemResponse($user);
else
return $this->errorResponse(['message' => 'Invalid credentials'], 401);
}
/**
* Received the returning object from the provider
* which we will use to resolve the user, we return the response in JSON format
*
* @return json
public function handleProviderCallbackApiUser(string $provider)
{
$socialite_user = Socialite::driver($provider)->stateless()->user();
if($user = OAuth::handleAuth($socialite_user, $provider))
{
return $this->itemResponse($user);
}
else if(MultiDB::checkUserEmailExists($socialite_user->getEmail()))
{
return $this->errorResponse(['message'=>'User exists in system, but not with this authentication method'], 400);
}
else {
//todo
$name = OAuth::splitName($socialite_user->getName());
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $socialite_user->getEmail(),
];
$account = CreateAccount::dispatchNow($new_account);
return $this->itemResponse($account->default_company->owner());
}
}
*/
}

View File

@ -11,6 +11,7 @@
namespace App\Http;
use App\Http\Middleware\ContactTokenAuth;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
@ -53,6 +54,11 @@ class Kernel extends HttpKernel
'bindings',
'query_logging',
],
'contact' => [
'throttle:60,1',
'bindings',
'query_logging',
],
'db' => [
\App\Http\Middleware\SetDb::class,
],
@ -84,6 +90,7 @@ class Kernel extends HttpKernel
'query_logging' => \App\Http\Middleware\QueryLogging::class,
'token_auth' => \App\Http\Middleware\TokenAuth::class,
'api_secret_check' => \App\Http\Middleware\ApiSecretCheck::class,
'contact_token_auth' => \App\Http\Middleware\ContactTokenAuth::class,
'contact_db' => \App\Http\Middleware\ContactSetDb::class,
];
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Middleware;
use App\Events\User\UserLoggedIn;
use App\Models\ClientContact;
use App\Models\CompanyToken;
use App\Models\User;
use Closure;
class ContactTokenAuth
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if( $request->header('X-API-TOKEN') && ($client_contact = ClientContact::with(['company'])->whereRaw("BINARY `token`= ?",[$request->header('X-API-TOKEN')])->first() ) )
{
//client_contact who once existed, but has been soft deleted
if(!$client_contact)
return response()->json(json_encode(['message' => 'Authentication disabled for user.'], JSON_PRETTY_PRINT) ,403);
//client_contact who has been disabled
if($client_contact->is_locked)
return response()->json(json_encode(['message' => 'Access is locked.'], JSON_PRETTY_PRINT) ,403);
//stateless, don't remember the contact.
auth()->guard('contact')->login($client_contact, false);
//event(new UserLoggedIn($user)); //todo
}
else {
return response()->json(json_encode(['message' => 'Invalid token'], JSON_PRETTY_PRINT) ,403);
}
return $next($request);
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Middleware;
use App\Libraries\MultiDB;
use App\Models\CompanyToken;
use Closure;
class SetContactDb
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$error['error'] = ['message' => 'Database could not be set'];
// we must have a token passed, that matched a token in the db, and multiDB is enabled.
// todo i don't think we can call the DB prior to setting it???? i think this if statement needs to be rethought
//if( $request->header('X-API-TOKEN') && (CompanyToken::whereRaw("BINARY `token`= ?",[$request->header('X-API-TOKEN')])->first()) && config('ninja.db.multi_db_enabled'))
if( $request->header('X-API-TOKEN') && config('ninja.db.multi_db_enabled'))
{
if(! MultiDB::contactFindAndSetDb($request->header('X-API-TOKEN')))
{
return response()->json(json_encode($error, JSON_PRETTY_PRINT) ,403);
}
}
else {
return response()->json(json_encode($error, JSON_PRETTY_PRINT) ,403);
}
return $next($request);
}
}

View File

@ -11,6 +11,7 @@
namespace App\Libraries;
use App\Models\ClientContact;
use App\Models\CompanyToken;
use App\Models\User;
@ -84,6 +85,25 @@ class MultiDB
return null;
}
public static function contactFindAndSetDb($token) :bool
{
foreach (self::$dbs as $db)
{
if($ct = ClientContact::on($db)->whereRaw("BINARY `token`= ?", [$token])->first())
{
self::setDb($ct->company->db);
return true;
}
}
return false;
}
public static function findAndSetDb($token) :bool
{

View File

@ -128,9 +128,7 @@ class Client extends BaseModel
public function getMergedSettings()
{
return ClientSettings::buildClientSettings(new CompanySettings($this->company->settings), new ClientSettings($this->settings));
}
public function documents()

View File

@ -11,6 +11,8 @@
namespace App\Models;
use App\Models\Company;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use Hashids\Hashids;
use Illuminate\Contracts\Auth\MustVerifyEmail;
@ -73,4 +75,14 @@ class ClientContact extends Authenticatable
$this->where('is_primary', true);
}
public function company()
{
return $this->belongsTo(Company::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@ -99,7 +99,7 @@ class Company extends BaseModel
*/
public function contacts()
{
return $this->hasMany(Contact::class);
return $this->hasMany(ClientContact::class);
}
/**

View File

@ -222,7 +222,7 @@ class User extends Authenticatable implements MustVerifyEmail
public function contacts()
{
return $this->hasMany(Contact::class);
return $this->hasMany(ClientContact::class);
}

View File

@ -227,10 +227,14 @@ Log::error($query->count());
private function setDefaultDatabase($id = false, $email = false, $token = false) : void
{
Log::error('setting DB');
Log::error('model = '.$this->model);
foreach (MultiDB::getDbs() as $database) {
$this->setDB($database);
$query = $this->conn->table('users');
// $query = $this->conn->table('users');
$query = $this->conn->table((new $this->model)->getTable());
if ($id)
$query->where('id', '=', $id);

View File

@ -136,7 +136,7 @@ class RouteServiceProvider extends ServiceProvider
$this->mapWebRoutes();
//
$this->mapContactApiRoutes();
}
/**
@ -168,6 +168,19 @@ class RouteServiceProvider extends ServiceProvider
->group(base_path('routes/api.php'));
}
/**
* Define the "api" routes for the application.
*
* These routes are typically stateless.
*
* @return void
*/
protected function mapContactApiRoutes()
{
Route::prefix('')
->middleware('contact')
->namespace($this->namespace)
->group(base_path('routes/contact.php'));
}
}

View File

@ -78,7 +78,7 @@ return [
],
'contacts' => [
'driver' => env('CONTACT_AUTH_PROVIDER', 'eloquent'),
'model' => App\Models\Contact::class,
'model' => App\Models\ClientContact::class,
],
// 'users' => [

View File

@ -21,7 +21,8 @@ $factory->define(App\Models\ClientContact::class, function (Faker $faker) {
'email_verified_at' => now(),
'email' => $faker->unique()->safeEmail,
'password' => bcrypt('password'),
'remember_token' => str_random(10)
'remember_token' => str_random(10),
'token' => str_random(64),
];
});

View File

@ -332,6 +332,8 @@ class CreateUsersTable extends Migration
$table->unsignedInteger('avatar_height')->nullable();
$table->unsignedInteger('avatar_size')->nullable();
$table->string('password');
$table->string('token')->nullable();
$table->boolean('is_locked')->default(false);
$table->rememberToken();
$table->timestamps(6);
$table->softDeletes();

View File

@ -0,0 +1,43 @@
<?php
namespace Tests\Unit;
use App\Utils\SystemHealth;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;
/**
* @test
* @covers App\Utils\SystemHealth
*/
class SystemHealthTest extends TestCase
{
public function setUp() :void
{
parent::setUp();
}
public function testVariables()
{
$results = SystemHealth::check();
$this->assertTrue(is_array($results));
$this->assertTrue(count($results) > 1);
$this->assertTrue($results['system_health']);
$this->assertTrue($results['extensions'][0]['mysqli']);
$this->assertTrue($results['extensions'][1]['gd']);
$this->assertTrue($results['extensions'][2]['curl']);
$this->assertTrue($results['extensions'][3]['zip']);
$this->assertTrue($results['dbs'][0]['db-ninja-01']);
}
}