spatie / laravel-multitenancy

Make your Laravel app usable by multiple tenants
https://spatie.be/docs/laravel-multitenancy
MIT License
1.1k stars 154 forks source link

[QUESTION] about managing tenants via "landlord" admin panel #64

Closed fermevc closed 4 years ago

fermevc commented 4 years ago

I've managed to migrate and seed landlord and two test tenants (tenant1.laravel.test, tenant2.laravel.test). I would like to have a couple of admin pages for "landlord" but I can't figure out how to do that on my own. These "landlord" pages would mainly be used for tenant management (simple admin panel for tenant CRUD operations). I think that because of "NeedsTennant" middleware, I can't serve anything on 'laravel.test' domain. When I try to open 'laravel.test', I get:

Spatie\Multitenancy\Exceptions\NoCurrentTenant
The request expected a current tenant but none was set.

I'm aware that this is expected behavior and can't even imagine if what I'm trying to do is possible or even if it should be done this way, mainly because I'm still a beginner.
What would be a proper way to implement "landlord" portion of application that would be accessible via 'laravel.test'. Thanks in advance!

masterix21 commented 4 years ago

See it: https://laravel.com/docs/7.x/routing#route-group-subdomain-routing

Use the grouped routes for your landlord domain.

fermevc commented 4 years ago

@masterix21 Thank you for fast response and suggestion! I've managed to move a little bit forward by using:

Route::domain('{tenant}.laravel.test')->middleware('tenant')->group(function () {
    Route::get('/', function () {
        return view('welcome');
    });
    Route::get('/home', 'HomeController@index')->name('home');
});

Route::domain('laravel.test')->group(function () {
    Route::get('/', function () {
        dd('hello landlord');
    });
});
Auth::routes(); 

and I also moved "NeedsTenant" middleware to a separate 'tenant' group. Tenant pages are working OK, including Auth routes. Now I just need to solve an issue regarding "landlord" Auth routes, as "login" currently gives me SQL error because "tenant" connection is used which is without "DB_NAME" and I need to find a way to switch to "landloard" connection when using 'laravel.test'.

freekmurze commented 4 years ago

Closing this as this is not an issue about the internals of the package.

Feel free to keep the conversation going.

masterix21 commented 4 years ago

@fermevc see here: https://github.com/spatie/laravel-multitenancy/pull/59#issuecomment-647804038

fermevc commented 4 years ago

@masterix21 Thanks for update! I've seen your proposal before posting my question, but didn't quite understood the logic, again, I'm still lacking the skills to dive deeper into Laravel... In the mean time, I've managed to do some tests with custom middleware:

class IsLandlord
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if ($request->getHost() == config('app.domain')) {
            // if Host is 'laravel.test' set DB connection to 'landlord'
            config(['database.default' => 'landlord']);
            return $next($request);
        } else {
            return $next($request);
        }
    }
}

Whit above in place, all tenant Auth routes and Home route are working OK, tenant switching is working OK, I can get to the landlord welcome and login views, but after submitting login form, 'landlord' connection is present throughout all methods until request comes to "attemptLogin()".

protected function attemptLogin(Request $request)
    {
        dd(config('database.default'));  // this still returns 'landlord'
        return $this->guard()->attempt(
            $this->credentials($request),
            $request->filled('remember')
        );
    }

If I comment "dd", I get SQL error because in SQL connector there's no "database" set: SQLSTATE[08006] [7] FATAL: database "port=5432" does not exist... I can't figure out why 'landlord' is lost at this moment. By inspecting the code for "guard()->attempt" I got totally lost, and cant go any deeper than this. I'll continue to look further, thank you once again for all the help and your time!

masterix21 commented 4 years ago

Sorry, but for me isn’t an excellent way to use a middleware. It’s better to create a database switch task.

uteq commented 4 years ago

@fermevc I got this working by extending the default NeedsTenant and EnsureValidTenantSession. There probably is a better solution for this. But for now this works.

First I used your middleware

class LandlordAsFallback
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, \Closure $next)
    {
        if ($request->getHost() === config('app.domain')) {

            // if Host is 'laravel.test' set DB connection to 'landlord'
            config(['database.default' => 'landlord']);

            return $next($request);
        } else {
            return $next($request);
        }
    }
}

Creating the LandlordAsFallback middleware is not enough because you will need to make sure that the entire tenant behavior is skipped whenever the landlord domain is requested. That way you can build your own 'application' next to that of the tenant. This probably has some pitfalls I am yet to discover.

Extend the NeedsTentant middleware

class NeedsTenant extends \Spatie\Multitenancy\Http\Middleware\NeedsTenant
{
    public function handle($request, Closure $next)
    {
        // Skip this middleware whenever the landlord is requested.
        if (config('database.default') === 'landlord') {
            return $next($request);
        }

        return parent::handle($request, $next);
    }
}

Extend the EnsureValidTenantSession

class EnsureValidTenantSession extends \Spatie\Multitenancy\Http\Middleware\EnsureValidTenantSession
{
    public function handle($request, Closure $next)
    {
        // Skip this middleware whenever the landlord is requested.
        if (config('database.default') === 'landlord') {
            return $next($request);
        }

        return parent::handle($request, $next);
    }
}

Than this should be the order in App\Http\Kernel

protected $middlewareGroups = [
    'web' => [
        ...
        \Support\Multitenancy\Middleware\LandlordAsFallback::class,
        \Support\Multitenancy\Middleware\NeedsTenant::class,
        \Support\Multitenancy\Middleware\EnsureValidTenantSession::class,
    ]
]

Hope this helps someone and if you have a better solution please share :)

masterix21 commented 4 years ago

@uteq, thanks for your share.

Veltix commented 3 years ago

I have same issue but these instructions didnt help me. Still getting error: "The request expected a current tenant but none was set." When trying to go top level domain.

Franco2911 commented 3 years ago

Hello, I followed different issues regarding multiple login and with this one all works fine except an issue on the login landlord routes.

I have tenant domain with subdomain.example.com and the landlord admin pages on app.example.com.

Using the code above In tenant mode all routes, login, filesystems etc.. works fine. In landlord mode all landlord routes works but if I use app.example.com/login (the same as tenant login route)I get an issue.

SQLSTATE[3D000]: Invalid catalog name: 1046 No database selected (SQL: select * fromsessionswhereid= VIm16ZNKypxFFLV2baoTVOP6ale0rjl4aYiiNYEu limit 1)

I am using an extended Startsession with this code:

class StartNewSession extends StartSession
{
    use UsesTenantModel;
    use UsesMultitenancyConfig;

    /**
     * Handle an incoming request.
     *
     * @param SessionManager $manager
     * @param callable|null $cacheFactoryResolver
     */
    public function __construct(Request $request, SessionManager $manager, callable $cacheFactoryResolver = null)
    {
        if (! Tenant::current()) {
            Config::set('session.connection', $this->landlordDatabaseConnectionName());
        } else {
            Config::set('session.connection', $this->tenantDatabaseConnectionName());
        }

        parent::__construct($manager, $cacheFactoryResolver);
    }

What I need to do is to have 2 different login for landlord and tenant with different datas and model for eachother.

it seems that when I call app.example.com/login laravel use the tenant route.

maybe we need to tell laravel to use the landlord routes ?

Thank you for help

masterix21 commented 3 years ago

@Franco2911, I think you are using the database session driver with a "tenant" database as the default connection in your DB_CONNECTION .env variable. Try to switch to landlord as default connection.

jaswinda commented 2 years ago

Just use in the AppServiceProvider boot function

if (Tenant::current()==null) {
    config(['database.default' => 'landlord']);
}
chrisvasey commented 2 years ago
if (Tenant::current()==null) {
    config(['database.default' => 'landlord']);
}

After jumping through many hoops with many different packages trying to get a multi-tenancy setup that works great for accessing the landlord app on the base domain and tenants on subdomains. Nova is working brilliantly on both after setting this up. This is the only change I needed to add on top of the standard documented setup of this package. Thank you so much for saving me more hours of pain, what a great solution!

To anyone stuck in a loop trying to work this out. Setup the landlord/tenant connections in your config/database.php, set the default to the tenant, and use this snippet in your AppServiceProvider to fall back to the landlord app. I know this isn't documented because Spatie are trying to stay unopinionated but I am pretty sure this is one of the most common use cases for a package like this.

lauhakari commented 1 year ago

Hey guys 👋

So I know this is kind of an old thread, with some different sloutions to slightly different problems. I found it when I was searching for a solution for when I have a Model that is used by both Tenants and Landlords, and had similar issues as above. So I just wanted to share my solution for it, which I think was pretty nice.

As you know use should use either UsesTenantConnection or UsesLandlordConnection to connect correct DB, but you can't use both!. So I created my own trait, in which I check if a Tenant is set and choose connection based in that. If you have a shared Model you can use this Trait, if your model is only for Tenant/Landlord you could use the corresponding Trait.

<?php

namespace App\Models\Traits;

use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig;
use Spatie\Multitenancy\Models\Tenant;

trait MultitenancyConnection
{
    use UsesMultitenancyConfig;

    public function getConnectionName()
    {
        if(Tenant::current()) {
            return $this->tenantDatabaseConnectionName();
        } else {
            return $this->landlordDatabaseConnectionName();
        }
    }
}

Works great with landlord on main domain, subdomain and tenants on subdomain. Hope it helps someone.

Mohamed-Elaraby commented 4 months ago

Hey guys 👋

So I know this is kind of an old thread, with some different sloutions to slightly different problems. I found it when I was searching for a solution for when I have a Model that is used by both Tenants and Landlords, and had similar issues as above. So I just wanted to share my solution for it, which I think was pretty nice.

As you know use should use either UsesTenantConnection or UsesLandlordConnection to connect correct DB, but you can't use both!. So I created my own trait, in which I check if a Tenant is set and choose connection based in that. If you have a shared Model you can use this Trait, if your model is only for Tenant/Landlord you could use the corresponding Trait.

<?php

namespace App\Models\Traits;

use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig;
use Spatie\Multitenancy\Models\Tenant;

trait MultitenancyConnection
{
    use UsesMultitenancyConfig;

    public function getConnectionName()
    {
        if(Tenant::current()) {
            return $this->tenantDatabaseConnectionName();
        } else {
            return $this->landlordDatabaseConnectionName();
        }
    }
}

Works great with landlord on main domain, subdomain and tenants on subdomain. Hope it helps someone.

where can i use this trait ?

masterix21 commented 4 months ago

where can i use this trait ?

In a model