mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 09:34:04 +00:00
Fix origin id w/ empty header & using full-hostname subdomain records
This makes it possible to have Domain records in both `foo` and
`foo.{centralDomain}` format when using the combined domain/subdomain
identification middleware, or the origin header id mw which extends it.
This commit also refactors some related logic.
This commit is contained in:
parent
c199a6e0c8
commit
56dd4117ab
7 changed files with 131 additions and 63 deletions
|
|
@ -71,6 +71,7 @@
|
||||||
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
|
"docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml",
|
||||||
"testbench-unlink": "rm ./vendor/orchestra/testbench-core/laravel/vendor",
|
"testbench-unlink": "rm ./vendor/orchestra/testbench-core/laravel/vendor",
|
||||||
"testbench-link": "ln -s vendor ./vendor/orchestra/testbench-core/laravel/vendor",
|
"testbench-link": "ln -s vendor ./vendor/orchestra/testbench-core/laravel/vendor",
|
||||||
|
"testbench-repair": "mkdir -p ./vendor/orchestra/testbench-core/laravel/storage/framework/sessions && mkdir -p ./vendor/orchestra/testbench-core/laravel/storage/framework/views && mkdir -p ./vendor/orchestra/testbench-core/laravel/storage/framework/cache",
|
||||||
"coverage": "open coverage/phpunit/html/index.html",
|
"coverage": "open coverage/phpunit/html/index.html",
|
||||||
"phpstan": "vendor/bin/phpstan",
|
"phpstan": "vendor/bin/phpstan",
|
||||||
"phpstan-pro": "vendor/bin/phpstan --pro",
|
"phpstan-pro": "vendor/bin/phpstan --pro",
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,12 @@ class InitializeTenancyByDomain extends IdentificationMiddleware
|
||||||
*/
|
*/
|
||||||
public function requestHasTenant(Request $request): bool
|
public function requestHasTenant(Request $request): bool
|
||||||
{
|
{
|
||||||
return ! in_array($this->getDomain($request), config('tenancy.identification.central_domains'));
|
$domain = $this->getDomain($request);
|
||||||
|
|
||||||
|
// Mainly used with origin identification if the header isn't specified and e.g. universal routes are used
|
||||||
|
if (! $domain) return false;
|
||||||
|
|
||||||
|
return ! in_array($domain, config('tenancy.identification.central_domains'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDomain(Request $request): string
|
public function getDomain(Request $request): string
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ use Closure;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
|
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
|
||||||
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
|
||||||
|
|
||||||
class InitializeTenancyByDomainOrSubdomain extends InitializeTenancyBySubdomain
|
class InitializeTenancyByDomainOrSubdomain extends InitializeTenancyBySubdomain
|
||||||
{
|
{
|
||||||
|
|
@ -23,34 +24,46 @@ class InitializeTenancyByDomainOrSubdomain extends InitializeTenancyBySubdomain
|
||||||
}
|
}
|
||||||
|
|
||||||
$domain = $this->getDomain($request);
|
$domain = $this->getDomain($request);
|
||||||
|
$subdomain = null;
|
||||||
|
|
||||||
if ($this->isSubdomain($domain)) {
|
if (DomainTenantResolver::isSubdomain($domain)) {
|
||||||
$domain = $this->makeSubdomain($domain);
|
$subdomain = $this->makeSubdomain($domain);
|
||||||
|
|
||||||
if ($domain instanceof Exception) {
|
if ($subdomain instanceof Exception) {
|
||||||
$onFail = static::$onFail ?? function ($e) {
|
$onFail = static::$onFail ?? function ($e) {
|
||||||
throw $e;
|
throw $e;
|
||||||
};
|
};
|
||||||
|
|
||||||
return $onFail($domain, $request, $next);
|
return $onFail($subdomain, $request, $next);
|
||||||
}
|
|
||||||
|
|
||||||
// If a Response instance was returned, we return it immediately.
|
|
||||||
// todo@samuel when does this execute?
|
|
||||||
if ($domain instanceof Response) {
|
|
||||||
return $domain;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->initializeTenancy(
|
try {
|
||||||
$request,
|
$this->tenancy->initialize(
|
||||||
$next,
|
$this->resolver->resolve($subdomain ?? $domain)
|
||||||
$domain
|
);
|
||||||
);
|
} catch (TenantCouldNotBeIdentifiedException $e) {
|
||||||
}
|
if ($subdomain) {
|
||||||
|
try {
|
||||||
|
$this->tenancy->initialize(
|
||||||
|
$this->resolver->resolve($domain)
|
||||||
|
);
|
||||||
|
} catch (TenantCouldNotBeIdentifiedException $e) {
|
||||||
|
$onFail = static::$onFail ?? function ($e) {
|
||||||
|
throw $e;
|
||||||
|
};
|
||||||
|
|
||||||
protected function isSubdomain(string $hostname): bool
|
return $onFail($e, $request, $next);
|
||||||
{
|
}
|
||||||
return Str::endsWith($hostname, config('tenancy.identification.central_domains'));
|
} else {
|
||||||
|
$onFail = static::$onFail ?? function ($e) {
|
||||||
|
throw $e;
|
||||||
|
};
|
||||||
|
|
||||||
|
return $onFail($e, $request, $next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ use Closure;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
|
use Stancl\Tenancy\Concerns\UsableWithEarlyIdentification;
|
||||||
use Stancl\Tenancy\Exceptions\NotASubdomainException;
|
use Stancl\Tenancy\Exceptions\NotASubdomainException;
|
||||||
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
|
||||||
class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
||||||
{
|
{
|
||||||
|
|
@ -57,20 +57,16 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return string|Response|Exception|mixed */
|
/** @return string|Exception */
|
||||||
protected function makeSubdomain(string $hostname)
|
protected function makeSubdomain(string $hostname)
|
||||||
{
|
{
|
||||||
$parts = explode('.', $hostname);
|
$parts = explode('.', $hostname);
|
||||||
|
|
||||||
$isLocalhost = count($parts) === 1;
|
|
||||||
$isIpAddress = count(array_filter($parts, 'is_numeric')) === count($parts);
|
$isIpAddress = count(array_filter($parts, 'is_numeric')) === count($parts);
|
||||||
|
|
||||||
// If we're on localhost or an IP address, then we're not visiting a subdomain.
|
|
||||||
$isACentralDomain = in_array($hostname, config('tenancy.identification.central_domains'), true);
|
$isACentralDomain = in_array($hostname, config('tenancy.identification.central_domains'), true);
|
||||||
$notADomain = $isLocalhost || $isIpAddress;
|
$thirdPartyDomain = ! DomainTenantResolver::isSubdomain($hostname);
|
||||||
$thirdPartyDomain = ! Str::endsWith($hostname, config('tenancy.identification.central_domains'));
|
|
||||||
|
|
||||||
if ($isACentralDomain || $notADomain || $thirdPartyDomain) {
|
if ($isACentralDomain || $isIpAddress || $thirdPartyDomain) {
|
||||||
return new NotASubdomainException($hostname);
|
return new NotASubdomainException($hostname);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use Stancl\Tenancy\Contracts\SingleDomainTenant;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
use Stancl\Tenancy\Tenancy;
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class DomainTenantResolver extends Contracts\CachedTenantResolver
|
class DomainTenantResolver extends Contracts\CachedTenantResolver
|
||||||
{
|
{
|
||||||
|
|
@ -55,6 +56,11 @@ class DomainTenantResolver extends Contracts\CachedTenantResolver
|
||||||
return $tenant;
|
return $tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function isSubdomain(string $domain): bool
|
||||||
|
{
|
||||||
|
return Str::endsWith($domain, config('tenancy.identification.central_domains'));
|
||||||
|
}
|
||||||
|
|
||||||
public function resolved(Tenant $tenant, mixed ...$args): void
|
public function resolved(Tenant $tenant, mixed ...$args): void
|
||||||
{
|
{
|
||||||
$this->setCurrentDomain($tenant, $args[0]);
|
$this->setCurrentDomain($tenant, $args[0]);
|
||||||
|
|
|
||||||
|
|
@ -6,62 +6,56 @@ use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
use Stancl\Tenancy\Database\Concerns\HasDomains;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||||||
use Stancl\Tenancy\Database\Models;
|
use Stancl\Tenancy\Database\Models;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
Route::group([
|
Route::group([
|
||||||
'middleware' => InitializeTenancyByDomainOrSubdomain::class,
|
'middleware' => InitializeTenancyByDomainOrSubdomain::class,
|
||||||
], function () {
|
], function () {
|
||||||
Route::get('/foo/{a}/{b}', function ($a, $b) {
|
Route::get('/test', function () {
|
||||||
return "$a + $b";
|
return tenant('id');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
config(['tenancy.models.tenant' => CombinedTenant::class]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant can be identified by subdomain', function () {
|
test('tenant can be identified by subdomain', function () {
|
||||||
config(['tenancy.identification.central_domains' => ['localhost']]);
|
config(['tenancy.identification.central_domains' => ['localhost']]);
|
||||||
|
|
||||||
$tenant = CombinedTenant::create([
|
$tenant = Tenant::create(['id' => 'acme']);
|
||||||
'id' => 'acme',
|
$tenant->domains()->create(['domain' => 'foo']);
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant->domains()->create([
|
|
||||||
'domain' => 'foo',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(tenancy()->initialized)->toBeFalse();
|
expect(tenancy()->initialized)->toBeFalse();
|
||||||
|
|
||||||
pest()
|
pest()->get('http://foo.localhost/test')->assertSee('acme');
|
||||||
->get('http://foo.localhost/foo/abc/xyz')
|
|
||||||
->assertSee('abc + xyz');
|
|
||||||
|
|
||||||
expect(tenancy()->initialized)->toBeTrue();
|
|
||||||
expect(tenant('id'))->toBe('acme');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant can be identified by domain', function () {
|
test('tenant can be identified by domain', function () {
|
||||||
config(['tenancy.identification.central_domains' => []]);
|
config(['tenancy.identification.central_domains' => []]);
|
||||||
|
|
||||||
$tenant = CombinedTenant::create([
|
$tenant = Tenant::create(['id' => 'acme']);
|
||||||
'id' => 'acme',
|
$tenant->domains()->create(['domain' => 'foobar.localhost']);
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant->domains()->create([
|
|
||||||
'domain' => 'foobar.localhost',
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(tenancy()->initialized)->toBeFalse();
|
expect(tenancy()->initialized)->toBeFalse();
|
||||||
|
|
||||||
pest()
|
pest()->get('http://foobar.localhost/test')->assertSee('acme');
|
||||||
->get('http://foobar.localhost/foo/abc/xyz')
|
|
||||||
->assertSee('abc + xyz');
|
|
||||||
|
|
||||||
expect(tenancy()->initialized)->toBeTrue();
|
|
||||||
expect(tenant('id'))->toBe('acme');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
class CombinedTenant extends Models\Tenant
|
test('domain records can be either in domain syntax or subdomain syntax', function () {
|
||||||
{
|
config(['tenancy.identification.central_domains' => ['localhost']]);
|
||||||
use HasDomains;
|
|
||||||
}
|
$foo = Tenant::create(['id' => 'foo']);
|
||||||
|
$foo->domains()->create(['domain' => 'foo']);
|
||||||
|
|
||||||
|
$bar = Tenant::create(['id' => 'bar']);
|
||||||
|
$bar->domains()->create(['domain' => 'bar.localhost']);
|
||||||
|
|
||||||
|
expect(tenancy()->initialized)->toBeFalse();
|
||||||
|
|
||||||
|
// Subdomain format
|
||||||
|
pest()->get('http://foo.localhost/test')->assertSee('foo');
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
// Domain format
|
||||||
|
pest()->get('http://bar.localhost/test')->assertSee('bar');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,12 @@ test('origin identification works', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('tenant routes are not accessible on central domains while using origin identification', function () {
|
test('tenant routes are not accessible on central domains while using origin identification', function () {
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
$tenant->domains()->create([
|
||||||
|
'domain' => 'foo',
|
||||||
|
]);
|
||||||
|
|
||||||
pest()
|
pest()
|
||||||
->withHeader('Origin', 'localhost')
|
->withHeader('Origin', 'localhost')
|
||||||
->post('home')
|
->post('home')
|
||||||
|
|
@ -54,3 +60,50 @@ test('onfail logic can be customized', function() {
|
||||||
->post('home')
|
->post('home')
|
||||||
->assertSee('onFail message');
|
->assertSee('onFail message');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('origin identification can be used with universal routes', function () {
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
$tenant->domains()->create([
|
||||||
|
'domain' => 'foo',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Route::post('/universal', function () {
|
||||||
|
return response(tenant('id') ?? 'central');
|
||||||
|
})->middleware([InitializeTenancyByOriginHeader::class, 'universal'])->name('universal');
|
||||||
|
|
||||||
|
pest()
|
||||||
|
->withHeader('Origin', 'foo.localhost')
|
||||||
|
->post('universal')
|
||||||
|
->assertSee($tenant->id);
|
||||||
|
|
||||||
|
tenancy()->end();
|
||||||
|
|
||||||
|
pest()
|
||||||
|
->withHeader('Origin', 'localhost')
|
||||||
|
->post('universal')
|
||||||
|
->assertSee('central');
|
||||||
|
|
||||||
|
pest()
|
||||||
|
// no header
|
||||||
|
->post('universal')
|
||||||
|
->assertSee('central');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('origin identification can be used with both domains and subdomains', function () {
|
||||||
|
$foo = Tenant::create();
|
||||||
|
$foo->domains()->create(['domain' => 'foo']);
|
||||||
|
|
||||||
|
$bar = Tenant::create();
|
||||||
|
$bar->domains()->create(['domain' => 'bar.localhost']);
|
||||||
|
|
||||||
|
pest()
|
||||||
|
->withHeader('Origin', 'foo.localhost')
|
||||||
|
->post('home')
|
||||||
|
->assertSee($foo->id);
|
||||||
|
|
||||||
|
pest()
|
||||||
|
->withHeader('Origin', 'bar.localhost')
|
||||||
|
->post('home')
|
||||||
|
->assertSee($bar->id);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue