Yii2 SaaS Authentication - database

I'm Developing SaaS application using Yii2 with separate DB architecture. I have a problem in login to system by using tenant database.
I need to get tenant database details from common db and establish tenant db connection after entering company id, username and password in login form.
This is my index.php file.
<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'dev');
require(__DIR__ . '/_protected/vendor/autoload.php');
require(__DIR__ . '/_protected/vendor/yiisoft/yii2/Yii.php');
$config = require(__DIR__ . '/_protected/config/web.php');
(new yii\web\Application($config));
if (Yii::$app->session->get('company')) :
$appConnection = \app\models\Userdbconnections::find()->where(['company_id' => Yii::$app->session->get('company')])->one();
\Yii::$app->dbDynamic->dsn = "mysql:host=localhost;dbname=$appConnection->dns";
\Yii::$app->dbDynamic->username = $appConnection->user;
\Yii::$app->dbDynamic->password = $appConnection->password;
\Yii::$app->dbDynamic->charset = 'utf8';
endif;
Yii::$app->run(); // this will run the application
?>
From login function after post logging data, auth controller is like this
if ( Yii::$app->request->post() ){
$connection = \app\models\Userdbconnections::find()->where(['company_id'=>Yii::$app->request->post('LoginForm')['company']])->one();
$_SESSION["dsn"] = $connection->dns;
$_SESSION["user"] = $connection->user;
$_SESSION["pass"] = $connection->password;
$_SESSION["company_id"] = $connection->company_id;
// Yii::$app->db()->close();
Yii::$app->set('db', [
'class' => '\yii\db\Connection',
'dsn' => "mysql:host=localhost;dbname={$connection->dns}",
'username' => $connection->user,
'password' => $connection->password,
]);
$model_db = new LoginForm();
$model_db->load(Yii::$app->request->post());
$model_db->login();
$_SESSION["login_user"] = $model_db->username;
}
User Management Module called in web.php under component part as following
'user' => [
'class' => 'webvimark\modules\UserManagement\components\UserConfig',
// Comment this if you don't want to record user logins
'on afterLogin' => function($event) {
\webvimark\modules\UserManagement\models\UserVisitLog::newVisitor($event->identity->id);
},
'enableSession' =>true,
],
Each model file consist with following code
public static function getDb()
{
return Yii::$app->get('dbDynamic');
}
So now i'm able to log from tenant db. But after checking i noticed User Management part, creation, role creation all these linked to common db when ever i logged in to tenant db. Is there anything I misses in here?

One way to do it is having two connections. One connection for common details coming from common database (user details, tenant db he belongs to, et al). This connection is static, so must be defined in the config (or just rename what comes with Yii Basic app to something like commonDb or use it with just db name.
Another one will be connected to the specific user tenant database. This will be dynamic and details must change. There are many ways to do it. One is to defined it before app runs. See this forum post for details. Another would be setting it up before request using Yii Container and call it inside your models et al. There might be other ways too.
So the process goes like this
User logs in. Connection used is the common connection (let it be defined as Yii::$app->db).
Using details from (1) create the dynamic connection.
Use the connections where needed (in models, Active data providers or Query builders)
Here is untested example
//common database with user login
----------------------------------
| id | name | tenant_database |
----------------------------------
| 1 | Stef | company_a |
----------------------------------
Note here that Yii::$app->user->identity will hold model class that wraps this table
//config/web.php
return [
'components' =>[
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=common_db',
'username' => 'username',
'password' => 'password',
'charset' => 'utf8',
]
'userDb' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=${database}',
'username' => 'username',
'password' => 'password',
'charset' => 'utf8',
]
]
//set it up before request
'on beforeRequest' => function ($event) {
if(Yii::$app->user->isGuest)
{
// redirect user to Login page
}
else
{
$currentDSN = Yii::$app->userDb->dsn;
$tenantDB = Yii::$app->user->identity->tenant_database;
Yii::$app->userDb->dsn = str_replace('${database}', $tenantDB, $currentDSN);
}
},
]
Then in model class override getDb as follows
class Data extends \yii\db\ActiveRecord
{
public static function getDb()
{
return Yii::$app->userDb;
}
}
Then user it as in:
$data = Data::find()->all();
$data = Yii::$app->userDb->createCommand('SELECT * FROM data')->queryAll();
UPDATE
Since OP wants the data to be in tenant db, the only way is having each tenant to have special Tenant Code, and on login page you will provide inputs for Tenant Code, Username and Password. Then
1. Query the common table for the database name associated with that code
2. Change Connection details as shown above
3. Login with TenantLogin class that uses tenant connection as shown above with Data class.
The new common table
----------------------------
| code | tenant_database |
----------------------------
| 12333 | company_a |
----------------------------

Related

Is there a better way to achieve multiple connection to database in Symfony 6?

I'm having some trouble trying to achieve multiple connection to database in some clean way.
Keep in mind that this is my first symfony project ever, and i'm only a young developer.
In my project, the goal is to be able to select a client, with a specific database, and to connect to the database to be able to export some datas.
I tried to do the solution describe in this post Symfony 3 connection to multiple databases and i tried to generate dynamically an entityManager.
So i created a factory EntityManagerFactory :
Factory\EntityManagerFactory
<?php
namespace App\Factory;
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Yaml\Yaml;
class EntityManagerFactory {
private $config_db_group;
public function __construct(string $config_db_group) {
$this->config_db_group = $config_db_group;
}
public function createManager($idDb) {
$isDevMode = false;
$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . "/src"), $isDevMode);
$connectionConfig = $this->getConfigDb($idDb);
$dbParams = [
'driver' => 'pdo_mysql',
'host' => $connectionConfig['host'],
'username' => $connectionConfig['user'],
'password' => $connectionConfig['password'],
'dbname' => $connectionConfig['db_name']
];
return EntityManager::create($dbParams, $config);
}
private function getConfigDb($idDb) {
$connectionConfig = Yaml::parseFile("$this->config_db_group");
return $connectionConfig[$idDb];
}
}
I have a yaml that describes the connection config :
config\dbgroup.yaml
1:
db_name: "db_name1"
host: "host1"
user: "user1"
password: "password1"
port: "3306"
2:
db_name: "db_name2"
host: "host2"
user: "user2"
password: "password2"
port: "3306"
In my config\services.yaml, i did something that was described in the post.
# Create a service for the factory
App\Factory\EntityManagerFactory:
arguments:
$config_db_group: '%kernel.project_dir%\config\db_group.yaml'
# Use the factory service as the first argument of the 'factory' option
# and the factory method as the second argument
App\Factory\EntityManager:
factory: ['#App\Factory\EntityManagerFactory', 'getManager']
I don't really understand what this is, i think this defines my factory as a service ? ...
And then i try to create an entityManager in my controller, this was just to test if it works, i think database request should be in a Repository, or a Services ?
<?php
namespace App\Controller;
use Twig\Environment;
use App\Factory\EntityManagerFactory;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use App\Repository\Istrator\DatabaseGroupRepository;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class DashboardController extends AbstractController {
private $twig;
private $databaseGroupRepository;
private $factory;
public function __construct(Environment $twig, DatabaseGroupRepository $databaseGroupRepository, EntityManagerFactory $factory) {
$this->twig = $twig;
$this->databaseGroupRepository = $databaseGroupRepository;
$this->factory = $factory;
}
#[Route('{slug}/dashboard', name: 'app_dashboard')]
public function index(string $slug): Response {
// I get the specific database
$databaseGroup = $this->databaseGroupRepository->findBySlug($slug);
// Then i try to create an entityManager with the correct config
$entityManager = $this->factory->createManager($databaseGroup->getIdDb());
// Then just to try my connection, i create a basic query
$rsm = new ResultSetMapping();
$test = $entityManager->createNativeQuery("USE mydatabase; SELECT * FROM mytable", $rsm)->execute();
return new Response($this->twig->render('dashboard/dashboard.html.twig', [
'controller_name' => 'DashboardController',
]));
}
}
For now it doesn't work.. But i have several questions :
The databases i try to connect are not database with the same database schemes than my actual database. They are external database. Should i create Entities, and repository to manage them ? Or should i just do some connection, some request without entity and repository ?
In the stackoverflow post that i based my code on, there is a second way of doing it, by defining all future connection in a doctrine.yaml. I have a defined number of connection but like 50 or something, should i do this instead of creating entityManager dynamically ?
As you can see, i'm a bit confused right now but if someone could tell me their point of vue, it would be great.
If you need any other information, just tell me !
Thanks in advance
EDIT :
I found the solution, and it was really stupid :
In my EntityManagerFactory, i did this :
$dbParams = [
'driver' => 'pdo_mysql',
'host' => $connectionConfig['host'],
// IT'S NOT USERNAME, IT'S USER ....
'username' => $connectionConfig['user'],
'password' => $connectionConfig['password'],
'dbname' => $connectionConfig['db_name']
];
In the StackOverflow post that is used to create this factory, it was written username, but the correct field was user.
That was my first mistake, my second mistake is that, when i tried to execute my nativeQuery, I created a resultSetMapping empty :
$entityManager = $this->factory->createManager($databaseGroup->getIdDb());
// I did this
$rsm = new ResultSetMapping();
$test = $entityManager->createNativeQuery("USE mydatabase; SELECT * FROM mytable", $rsm)->execute();
// I SHOULD HAVE DONE THIS
$rsm = new ResultSetMappingBuilder($entityManager);
$rsm->addScalarResult('id', 'id');
[... for every field]
$result = $entityManager->createNativeQuery("SELECT id, prenom, nom FROM mytable",$rsm)->execute();
I use addScalarResult because what i get from those databases are not Entities I will keep in my program.
I hope if someone get stuck like me, this could help him/her/etc..

Cakephp authentication plugin how can I add difference session key?

In present scenario after login in front end if I visit /admin prefix. It's accessing admin panel. Here I'm using difference model for login. For front end I'm using users table and for admin I'm using admin_users table. I have made this changes in application.php like
if($request->getParam('prefix') == 'Admin')
{
$identifierSettings += [
'resolver' => [
'className' => 'Authentication.Orm',
'userModel' => 'AdminUsers',
],
];
}
How could I add difference session key for admin and front-end ?
In Authentication.Session
Set your session key for admin 'sessionKey' => 'Auth.admin'
Note : Default sessionKey is Auth
Details : https://book.cakephp.org/authentication/2/en/authenticators.html#session

Drupal 8 - How to switch to external database in custom module?

I am trying to switch to and query a external database in a custom Drupal 8 module I have created.
I have added the external database below the native database in settings.php:
// Add second database
$databases['external']['default'] = array(
'database' => 'uconomy_external',
'username' => 'uconomy_admin',
'password' => 'fNjA9kC35h8',
'prefix' => '',
'host' => 'localhost',
'port' => '3306',
'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql',
'driver' => 'mysql',
);
I then have a file named BusinessListingDbLogic.php where I make queries to the database :
<?php
namespace Drupal\business_listing;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Database;
/**
* Defines a storage handler class that handles the node grants system.
*
* This is used to build node query access.
*
* This class contains all the logic for interacting with our database
*
* #ingroup business_listing
*/
class BusinessListingDbLogic {
/**
* #var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* #param \Drupal\Core\Database\Connection $connection
*/
public function __construct(Connection $connection) {
$this->database = $connection;
//Database::setActiveConnection('external');
}
/**
* Add new record in table business_listing.
*/
public function add($title, $body, $imageName, $location, $email) {
if (empty($title) || empty($body) || empty($imageName) || empty($location) || empty($email)) {
return FALSE;
}
// add record to business_listing table in database.
$query = $this->database->insert('business_listing');
$query->fields(array(
'title' => $title,
'body' => $body,
'image' => $imageName,
'location' => $location,
'email' => $email
));
return $query->execute();
}
I believe my BusinessListingDbLogic class is registered as a service, my business_listing.services.yml looks as follows:
services:
# Service Name.
business_listing.database.external:
class: Drupal\Core\Database\Connection
factory: 'Drupal\Core\Database\Database::getConnection'
arguments: ['external']
# external database dependent serivce.
business_listing.db_logic:
# Class that renders the service.
# BusinessListingDbLogic contains all the functions we use to interact with the business_listings table
class: Drupal\business_listing\BusinessListingDbLogic
# Arguments that will come to the class constructor.
arguments: ['#business_listing.database.external']
# A more detailed explanation: https://www.drupal.org/node/2239393.
# tags:
# - { name: backend_overridable }
This code works until I try uncomment Database::setActiveConnection('external');
I then get the following error:
The website encountered an unexpected error. Please try again later.Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'uconomy_external.shortcut_set_users' doesn't exist: SELECT ssu.set_name AS set_name
FROM
{shortcut_set_users} ssu
WHERE ssu.uid = :db_condition_placeholder_0; Array
(
[:db_condition_placeholder_0] => 1
)
it looks like the switch is working, but Drupal might be trying to use the external database for its native functionality? I know I also have to switch back to the default database at some point, but I am not sure where to do this?
Any help or advice would be GREATLY appreciate. Kind Regards, Matt
Seems that instead of calling your current connection to set, you need to use the static method Database::setActiveConnection() directly.
Eg. $this->database->setActiveConnection('external') becomes Database::setActiveConnection('external')

Laravel connec to database with user inputed username and password

I'm trying to make some kind of a installer for a CMS I'm working on.
The installer page is basically a page where the user inputs the database host, port, username, password and schema name. How could I use this inputed data to test if I actually can connect with the inputs given?
You can get the user inputs into a controller function and update the config/datadata.php. Assuming the driver is mysql by default, you can do this:
public function checkDatabaseConnection(Request $request)
{
//update the config
config(['database.connections.mysql' => [
'host' => $request->host,
'username' => $request->username,
'password' => $request->password
]]);
//Check the credentials by calling PDO
try {
DB::connection()->getPdo();
} catch (\Exception $e) {
return redirect()->back()->withErrors(["connection" => "Could not connect to the database. Please check your input."]);
}
}
Don't forget to add use DB at the top of your controller.

CakePHP + Facebook

I am trying to implement facebook Connect to my cakephp Application. i am using Nick's Facebook Plugin.
I wanna implement it this way
When a user Visits the Site he should be able to login via Registration on the site or Facebook Connect
Existing users should be able to connect their account to their FB account
People who first time login to the site using FB Connect and dont have an account on the site. should be redirected to a page where they have to enter details to complete the profile.
What i have done -
I have followed the instruction of Nick to implement it and when i click Login - it connects to my app. but i dont understand how to create a username and password associated with the Fb Connect Id. and user it against the FB token.
Apparently I'm doing the same thing a little before you... ;-)
Here's a method for Facebook login I'm using (slightly redacted and annotated):
public function facebook($authorize = null) {
App::import('Lib', 'Facebook.FB');
$Fb = new FB();
$session = $Fb->getSession();
// not logged into Facebook and not a callback either,
// sending user over to Facebook to log in
if (!$session && !$authorize) {
$params = array(
'req_perms' => /* the permissions you require */,
'next' => Router::url(array('action' => 'facebook', 'authorize'), true),
'cancel_url' => Router::url(array('action' => 'login'), true)
);
$this->redirect($Fb->getLoginUrl($params));
}
// user is coming back from Facebook login,
// assume we have a valid Facebook session
$userInfo = $Fb->api('/me');
if (!$userInfo) {
// nope, login failed or something went wrong, aborting
$this->Session->setFlash('Facebook login failed');
$this->redirect(array('action' => 'login'));
}
$user = array(
'User' => array(
'firstname' => $userInfo['first_name'],
'lastname' => $userInfo['last_name'],
'username' => trim(parse_url($userInfo['link'], PHP_URL_PATH), '/'),
'email' => $userInfo['email'],
'email_validated' => $userInfo['verified']
),
'Oauth' => array(
'provider' => 'facebook',
'provider_uid' => $userInfo['id']
)
);
$this->oauthLogin($user);
}
This gives me an array with all the user details I could grab from Facebook and invokes ::oauthLogin, which either logs the user in with the given information or asks the user to fill in missing details and/or creates a new user record in the database. The most important part you get from the Facebook API is the $userInfo['id'] and/or email address, either of which you can use to identify the user in your database. If you're using the AuthComponent, you can "manually" log in the user using $this->Auth->login($user_id), where $user_id is the id of the user in your own database.
private function oauthLogin($data) {
$this->User->create();
// do we already know about these credentials?
$oauth = $this->User->Oauth->find('first', array('conditions' => $data['Oauth']));
if ($oauth) {
// yes we do, let's try to log this user in
if (empty($oauth['User']['id']) || !$this->Auth->login($oauth['User']['id'])) {
$this->Session->setFlash('Login failed');
}
$this->redirect('/');
}
// no we don't, let's see if we know this email address already
if (!empty($data['User']['email'])) {
$user = $this->User->find('first', array('conditions' => array('email' => $data['User']['email'])));
if ($user) {
// yes we do! let's store all data in the session
// and ask the user to associate his accounts
$data['User'] = array_merge($data['User'], $user['User']);
$data['Oauth']['user_id'] = $user['User']['id'];
$this->Session->write('Oauth.associate_accounts', $data);
$this->redirect(array('action' => 'oauth_associate_accounts'));
}
}
// no, this is a new user, let's ask him to register
$this->Session->write('Oauth.register', $data);
$this->redirect(array('action' => 'oauth_register'));
}
Look no further. Here is an excellent article that'll guide you all the way through (minus any readymade plugins):
Integrating Facebook Connect with CakePHP's Auth component
Simply follow the approach described in there.
Cheers,
m^e

Resources