Home > OS >  Can we use multiple second-level domains with multitenancy?
Can we use multiple second-level domains with multitenancy?

Time:06-22

I have implemented the simplest example using the Spatie docs for multitenancy, that is working perfectly fine. Now, I intend to use multiple second-level domains for each tenant I have.

For example; I have 2 tenants company-a and company-b and they are being served at company-a.localhost and company-b.localhost, now what I want is that when I visit company-a.admin.localhost, it should tell me COMPANY-A ADMIN and If I visit company-a.employee.localhost, it should tell me COMPANY-A EMPLOYEE.

I have tried using subdomain on routes in RouteServiceProvider like the following:

           Route::middleware('web')
                ->group(base_path('routes/security.php'));

           Route::domain($this->baseDomain('admin'))
                ->middleware('web')
                ->name('admin.')
                ->group(base_path('routes/admin.php'));

           Route::domain($this->baseDomain('employee'))
                ->middleware('web')
                ->name('employee.')
                ->group(base_path('routes/employee.php'));

           private function baseDomain(string $subdomain = ''): string
           {
             if (strlen($subdomain) > 0) {
                $subdomain = "{$subdomain}.";
              }
             return $subdomain . config('app.base_domain');
           }

Without subdomain, it works fine, but the routes with second-level domain, it falls to base level domain route and does not get the current tenant. What am I missing here? Is this even possible to implement.

Thankyou.

CodePudding user response:

Take, for example, the route:

 Route::domain('{subdomain}.example.com')
   ->get('/foo/{param1}/{param2}',function(Router $router) {
       // do something with it
   });

The binding fields would be ['subdomain', 'param1', 'param2'], and the compiled route would have it's regexes declared as

regex => "{^/foo/(?P<param1>[^/]  )/(?P<param2>[^/]  )$}sDu",
hostRegex => "{^(?P<subdomain>[^\.]  )\.example\.com$}sDiu"

Where ^(?P<subdomain>[^\.] )\. will explicitly stop capturing when finding a dot, in this case the delimiter between groups.

However, these regexes are overridable by using the where method. You could declare the above route as

 Route::domain('{subdomain}.example.com')
   ->get('/foo/{param1}/{param2}',function(Router $router) {
       // do something with it
   })->where('subdomain', '(.*)');

In the compiled route , the hostRegex would be now

 hostRegex => "{^(?P<subdomain>(?:.*))\.example\.com$}sDiu"

Meaning it will capture anything preceding .example.com. If you requested company-a.admin.example.com, $subdomain would be company-a.admin.

You could also declare a route with two binding fields in its domain:

 Route::domain('{subsubdomain}.{subdomain}.example.com')
   ->get('/foo/{param1}/{param2}',function(Router $router) {
       // do something with it
   });

Which might be more useful if you wanted subsubdomains to imply a hierarchy.

CodePudding user response:

I have achieved this by using some checks, in RouteServiceProvider, I have not used the actual domain function on Route like we do normally i.e. Route::domain('foo.bar'). The reason was that, the Spatie package use a kind of middleware Spatie\Multitenancy\TenantFinder\DomainTenantFinder::class which runs whenever we hit the domain with tenant comapny-a.localhost. And it gets the tenant from hostname i.e comapny-a.localhost.

    public function findForRequest(Request $request):?Tenant
    {
        $host = $request->getHost();
        return $this->getTenantModel()::whereDomain($host)->first();
    }

In my RouteServiceProvide:

     $this->routes(function () {
         $class = 'security';
         $middleware = 'web';
         if (Str::contains(request()->getHost(), 'admin')) {
             $class = 'admin';
         } elseif (Str::contains(request()->getHost(), 'employee')) {
             $class = 'employee';
         } elseif (Str::contains(request()->getHost(), 'api')) {
             $class = 'api';
             $middleware = 'api';
         }

         Route::middleware($middleware)
             ->name("$class.")
             ->group(base_path("routes/${class}.php"));
     });

As In my scenario, I had only these 2 kind of second-level domains and so, I just checked if this particular keyword exists in the hostname and choosing the file and middleware accordingly.

I also overrided the DomainTenantFinder class and in multitenancy.php config file:

    public function findForRequest(Request $request): ?Tenant
    {
        $host = $request->getHost();
        $host = str_replace('admin.', '', $host);
        $host = str_replace('employee.', '', $host);
        $host = str_replace('api.', '', $host);
        $tenant = $this->getTenantModel()::whereDomain($host)->first();
        if (empty($tenant)) {
            abort(404);
        }
        return $tenant;
    }

I have acheived the desired outcome, however, I have a security concern, specially in RouteServiceProvider logic. Thought??

  • Related