mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Working on client login routes
This commit is contained in:
parent
83f6a88cb3
commit
51b0c17c4c
308
app/Http/Controllers/Contact/LoginController.php
Normal file
308
app/Http/Controllers/Contact/LoginController.php
Normal 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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
58
app/Http/Middleware/ContactTokenAuth.php
Normal file
58
app/Http/Middleware/ContactTokenAuth.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
58
app/Http/Middleware/SetContactDb.php
Normal file
58
app/Http/Middleware/SetContactDb.php
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
{
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ class Company extends BaseModel
|
||||
*/
|
||||
public function contacts()
|
||||
{
|
||||
return $this->hasMany(Contact::class);
|
||||
return $this->hasMany(ClientContact::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -222,7 +222,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
public function contacts()
|
||||
{
|
||||
|
||||
return $this->hasMany(Contact::class);
|
||||
return $this->hasMany(ClientContact::class);
|
||||
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ return [
|
||||
],
|
||||
'contacts' => [
|
||||
'driver' => env('CONTACT_AUTH_PROVIDER', 'eloquent'),
|
||||
'model' => App\Models\Contact::class,
|
||||
'model' => App\Models\ClientContact::class,
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
|
@ -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),
|
||||
];
|
||||
|
||||
});
|
||||
|
@ -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();
|
||||
|
43
tests/Unit/SystemHealthTest.php
Normal file
43
tests/Unit/SystemHealthTest.php
Normal 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']);
|
||||
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user