archtechx / tenancy

Automatic multi-tenancy for Laravel. No code changes needed.
https://tenancyforlaravel.com
MIT License
3.66k stars 429 forks source link

Getting data from the central domain database rather than the tenants database. #558

Closed cptiwari20 closed 3 years ago

cptiwari20 commented 3 years ago

Describe the bug

I have created a model and migration called PaymentSettings for both tenants and central domain users.

For central Users = app\Models\System\Admin\PaymentSettings.php For tenants users = app\Models\Tenant\Admin\PaymentSettings.php

I have added the crud operation which is running absolutely fine. I am facing the challenge while setting up the stripe API on boot, in the service provider. When I am fetching the settings data from the database

File: app\Providers\TenancyServiceProvider.php

namespace App\Providers;
use App\Models\System\Admin\SmtpSettings as AdminSmtpSettings;
use App\Models\Tenant\Admin\AdvanceSettings;
use App\Models\Tenant\Admin\GeneralSettings;
use App\Models\Tenant\Admin\PaymentSettings;
use App\Models\Tenant\Admin\SmtpSettings;
 ...

public function boot()
    {
        $this->bootEvents();
        $this->mapRoutes();        
        $this->makeTenancyMiddlewareHighestPriority();
        $this->viewResource();
    }
 ....
   protected function viewResource(){
        View::composer('*', function ($view) {
            $this->setConfigData();
            if(Schema::hasTable('general_settings')){
                $generalSettings = GeneralSettings::findOrFail(1);
                $advanceSettings = AdvanceSettings::findOrFail(1);
                $view->with([
                        'tenant_general' => $generalSettings,
                        'tenant_advance' => $advanceSettings,
                ]);
            }
        });
    }
 protected function setConfigData()
    {

        // When tenancy exists fetch the data from the tenants data;
        $smtpTableExists = Schema::hasTable('smtp_settings');
        $smtpSettings = null;
        if(tenancy()->tenant){
            if($smtpTableExists){  $smtpSettings = SmtpSettings::findOrFail(1); }

            // Stripe Payment settings
            $paymentTableExists = Schema::hasTable('payment_settings');
            if($paymentTableExists){
                $paymentSettings = PaymentSettings::findOrFail(1);
                // return dd($paymentSettings->key);
                if($paymentSettings && $paymentSettings->method === 'Stripe'){
                    Config::set('services.stripe.key', $paymentSettings->key);
                    Config::set('services.stripe.secret', $paymentSettings->secret);
                    Config::set('services.stripe.webhook_secret', $paymentSettings->webhook_secret);
                    Stripe::setApiKey($paymentSettings->key);
                }
            }
        } // ends tenants config
        else { // fetch data from the central domain
            if($smtpTableExists){  $smtpSettings = AdminSmtpSettings::findOrFail(1); }
        }

        // set configs
        if($smtpSettings){
            Config::set('mail.mailers.smtp.host', $smtpSettings->host);
            Config::set('mail.mailers.smtp.port', $smtpSettings->port);
            Config::set('mail.mailers.smtp.encryption', $smtpSettings->encryption);
            Config::set('mail.mailers.smtp.username', $smtpSettings->username);
            Config::set('mail.mailers.smtp.password', $smtpSettings->password);
            Config::set('mail.from.address', $smtpSettings->from_address);
            Config::set('mail.from.name', $smtpSettings->from_name);
        }

    }
....

As I have already set up the API key, I should get this inside the controllers, but is not so, I am getting the value null inside the other controllers when I am calling Stripe::getApiKey()

I thought maybe the boot file is not running properly, to I tried to manually write this inside the controllers' constructor inside the tenant's domain as

use App\Models\Tenant\Admin\PaymentSettings;

public function __construct()
    {
        $this->middleware('auth');
        $paymentSettings = PaymentSettings::findOrFail(1);
        Stripe::setApiKey($paymentSettings->key);
       return dd(Stripe::getApiKey()); // showing the incorrect value from the central domain
   } 

But the problem did not end here, The value that I am getting is the value from the Central domain not the tenant domain inside the controllers. BUT when I am writing a similar thing inside any other method in the tenant's controller, it's giving the correct value.

use App\Models\Tenant\Admin\PaymentSettings;

public function index()
    {
         // return dd(Stripe::getApiKey()); // showing the incorrect correct value from central domain

        $paymentSettings = PaymentSettings::findOrFail(1);
        Stripe::setApiKey($paymentSettings->key);

          return dd(Stripe::getApiKey()); // showing the correct value

        return view(someviewfilename);
   } 

Steps to reproduce

Expected behavior

I want to fetch the tenant's database data when I am on the tenant's domain, not the central domain DB data.

Your setup

stancl commented 3 years ago

Where do you want to fetch the data? The boot method and controller constructors obviously won't work because the app doesn't know what tenant is being used yet.

cptiwari20 commented 3 years ago

I want to use the data while creating?Editing/deleting new plans and other payment stuff. Mainly for Cashier stuff.

stancl commented 3 years ago
$this->viewResource();
    }
 ....
   protected function viewResource(){
        View::composer('*', function ($view) {
            $this->setConfigData();
            if(Schema::hasTable('general_settings')){
                $generalSettings = GeneralSettings::findOrFail(1);
                $advanceSettings = AdvanceSettings::findOrFail(1);
                $view->with([
                        'tenant_general' => $generalSettings,
                        'tenant_advance' => $advanceSettings,
                ]);
            }
        });
    }

This will always run in the central context, because you're accessing the data in a service provider.

cptiwari20 commented 3 years ago

But why this is happening, as there is already the middleware has been initialized to prevent access from the central domain. And why this is happening within the constructor function inside the controller. At the same time it is working inside the other methods of controllers.

public function events()
    {
        return [
            // Tenant events
            Events\CreatingTenant::class => [],
            Events\TenantCreated::class => [
                JobPipeline::make([
                    Jobs\CreateDatabase::class,
                    Jobs\MigrateDatabase::class,
                    // Jobs\SeedDatabase::class,

                    // Your own jobs to prepare the tenant.
                    // Provision API keys, create S3 buckets, anything you want!

                ])->send(function (Events\TenantCreated $event) {
                    return $event->tenant;
                })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
            ],
            Events\SavingTenant::class => [],
            Events\TenantSaved::class => [],
            Events\UpdatingTenant::class => [],
            Events\TenantUpdated::class => [],
            Events\DeletingTenant::class => [],
            Events\TenantDeleted::class => [
                JobPipeline::make([
                    Jobs\DeleteDatabase::class,
                ])->send(function (Events\TenantDeleted $event) {
                    return $event->tenant;
                })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
            ],

            // Domain events
            Events\CreatingDomain::class => [],
            Events\DomainCreated::class => [],
            Events\SavingDomain::class => [],
            Events\DomainSaved::class => [],
            Events\UpdatingDomain::class => [],
            Events\DomainUpdated::class => [],
            Events\DeletingDomain::class => [],
            Events\DomainDeleted::class => [],

            // Database events
            Events\DatabaseCreated::class => [],
            Events\DatabaseMigrated::class => [],
            Events\DatabaseSeeded::class => [],
            Events\DatabaseRolledBack::class => [],
            Events\DatabaseDeleted::class => [],

            // Tenancy events
            Events\InitializingTenancy::class => [],
            Events\TenancyInitialized::class => [
                Listeners\BootstrapTenancy::class,
            ],

            Events\EndingTenancy::class => [],
            Events\TenancyEnded::class => [
                Listeners\RevertToCentralContext::class,
            ],

            Events\BootstrappingTenancy::class => [],
            Events\TenancyBootstrapped::class => [],
            Events\RevertingToCentralContext::class => [],
            Events\RevertedToCentralContext::class => [],

            // Resource syncing
            Events\SyncedResourceSaved::class => [
                Listeners\UpdateSyncedResource::class,
            ],

            // Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
            Events\SyncedResourceChangedInForeignDatabase::class => [],
        ];
    }

    public function register()
    {
        //
    }

    public function boot()
    {
        $this->bootEvents();
        $this->mapRoutes();        
        $this->makeTenancyMiddlewareHighestPriority();
        $this->viewResource();
    }

    protected function bootEvents()
    {
        foreach ($this->events() as $event => $listeners) {
            foreach (array_unique($listeners) as $listener) {
                if ($listener instanceof JobPipeline) {
                    $listener = $listener->toListener();
                }

                Event::listen($event, $listener);
            }
        }
    }

    protected function mapRoutes()
    {
        if (file_exists(base_path('routes/tenant.php'))) {
            Route::namespace('App\Http\Controllers')
                ->group(base_path('routes/tenant.php'));
        }
    }

    protected function makeTenancyMiddlewareHighestPriority()
    {
        $tenancyMiddleware = [
            // Even higher priority than the initialization middleware
            Middleware\PreventAccessFromCentralDomains::class,

            Middleware\InitializeTenancyByDomain::class,
            Middleware\InitializeTenancyBySubdomain::class,
            Middleware\InitializeTenancyByDomainOrSubdomain::class,
            Middleware\InitializeTenancyByPath::class,
            Middleware\InitializeTenancyByRequestData::class,
        ];

        foreach (array_reverse($tenancyMiddleware) as $middleware) {
            $this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
        }
    }

    protected function viewResource(){
        View::composer('*', function ($view) {
            $this->setConfigData();
            if(Schema::hasTable('general_settings')){
                $generalSettings = GeneralSettings::findOrFail(1);
                $advanceSettings = AdvanceSettings::findOrFail(1);
                $view->with([
                        'tenant_general' => $generalSettings,
                        'tenant_advance' => $advanceSettings,
                ]);
            }
        });
    }
stancl commented 3 years ago

But why this is happening, as there is already the middleware has been initialized to prevent access from the central domain.

Think about how the Laravel request lifecycle works.

Here you're defining some middleware specifics:

Screen Shot 2020-12-11 at 16 29 13

And on the next line, you expect the middleware to be already executed? Of course it's not, Laravel first executes service providers and controller constructors, and only then does it know what middleware to use.

And why this is happening within the constructor function inside the controller. At the same time it is working inside the other methods of controllers.

Please read the early identification docs page.

cptiwari20 commented 3 years ago

Thanks

sirhB commented 3 years ago

I am having the same issue. I have spent some time reading through all the docs but I am still unable to understand how to make called controllers within routes/tenant.php access the tenant database rather than the central database.

sirhB commented 3 years ago

My route looks like this for tenants:

Route::middleware([
    'web',
    'auth',
    InitializeTenancyBySubdomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {
    Route::get('/events', 'EventController@displayEvents')->name('events');
    // other routes here
});

I added this __construct function to my EventController (after calling use App\Models\Event, ...):

public function __construct(Event $events, User $users)
    {
        $this->users = $users;
        $this->events = $events;
    }

I have a function within the controller that checks the user role

 public function roleCheck()
    {
        $user = $this->users->find(Auth::id());
        if($user->hasAnyRole('Super Administrator', 'Administrator'))
        {
            return 'admin';
        }
        elseif($user->hasAnyRole('Client'))
        {
            return 'client';
        }
        else
        {
            return false;
        }
    }

Here is the Flare of the error. http://flareapp.io/share/17Dn3k9m