mirror of
https://github.com/archtechx/tenancy.git
synced 2025-12-12 22:54:03 +00:00
Merge branch 'master' into cache-prefix
This commit is contained in:
commit
ba8cfdda85
38 changed files with 631 additions and 362 deletions
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
|
@ -107,6 +107,11 @@ jobs:
|
||||||
name: Static analysis (PHPStan)
|
name: Static analysis (PHPStan)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.2'
|
||||||
|
extensions: imagick, swoole
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install composer dependencies
|
- name: Install composer dependencies
|
||||||
run: composer install
|
run: composer install
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
# add amd64 platform to support Mac M1
|
# add amd64 platform to support Mac M1
|
||||||
FROM --platform=linux/amd64 shivammathur/node:latest-amd64
|
FROM --platform=linux/amd64 shivammathur/node:latest-amd64
|
||||||
|
|
||||||
# todo update this to 8.2 once shivammathur/node supports that
|
ARG PHP_VERSION=8.2
|
||||||
ARG PHP_VERSION=8.1
|
|
||||||
|
|
||||||
WORKDIR /var/www/html
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,16 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
Jobs\CreateDatabase::class,
|
Jobs\CreateDatabase::class,
|
||||||
Jobs\MigrateDatabase::class,
|
Jobs\MigrateDatabase::class,
|
||||||
// Jobs\SeedDatabase::class,
|
// Jobs\SeedDatabase::class,
|
||||||
Jobs\CreateStorageSymlinks::class,
|
|
||||||
|
// Jobs\CreateStorageSymlinks::class,
|
||||||
|
|
||||||
// Your own jobs to prepare the tenant.
|
// Your own jobs to prepare the tenant.
|
||||||
// Provision API keys, create S3 buckets, anything you want!
|
// Provision API keys, create S3 buckets, anything you want!
|
||||||
|
|
||||||
])->send(function (Events\TenantCreated $event) {
|
])->send(function (Events\TenantCreated $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
||||||
|
|
||||||
|
// Listeners\CreateTenantStorage::class,
|
||||||
],
|
],
|
||||||
Events\SavingTenant::class => [],
|
Events\SavingTenant::class => [],
|
||||||
Events\TenantSaved::class => [],
|
Events\TenantSaved::class => [],
|
||||||
|
|
@ -53,7 +55,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
Events\TenantDeleted::class => [
|
Events\TenantDeleted::class => [
|
||||||
JobPipeline::make([
|
JobPipeline::make([
|
||||||
Jobs\DeleteDatabase::class,
|
Jobs\DeleteDatabase::class,
|
||||||
Jobs\RemoveStorageSymlinks::class,
|
// Jobs\RemoveStorageSymlinks::class,
|
||||||
])->send(function (Events\TenantDeleted $event) {
|
])->send(function (Events\TenantDeleted $event) {
|
||||||
return $event->tenant;
|
return $event->tenant;
|
||||||
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
})->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production.
|
||||||
|
|
@ -153,7 +155,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
protected function makeTenancyMiddlewareHighestPriority()
|
protected function makeTenancyMiddlewareHighestPriority()
|
||||||
{
|
{
|
||||||
// PreventAccessFromCentralDomains has even higher priority than the identification middleware
|
// PreventAccessFromCentralDomains has even higher priority than the identification middleware
|
||||||
$tenancyMiddleware = array_merge([Middleware\PreventAccessFromCentralDomains::class], config('tenancy.identification.middleware'));
|
$tenancyMiddleware = array_merge([Middleware\PreventAccessFromUnwantedDomains::class], config('tenancy.identification.middleware'));
|
||||||
|
|
||||||
foreach (array_reverse($tenancyMiddleware) as $middleware) {
|
foreach (array_reverse($tenancyMiddleware) as $middleware) {
|
||||||
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
|
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ return [
|
||||||
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
|
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
|
||||||
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
|
Stancl\Tenancy\Bootstrappers\BatchTenancyBootstrapper::class,
|
||||||
// Stancl\Tenancy\Bootstrappers\PrefixCacheTenancyBootstrapper::class,
|
// Stancl\Tenancy\Bootstrappers\PrefixCacheTenancyBootstrapper::class,
|
||||||
|
// Stancl\Tenancy\Bootstrappers\SessionTenancyBootstrapper::class,
|
||||||
// Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper::class, // Queueing mail requires using QueueTenancyBootstrapper with $forceRefresh set to true
|
// Stancl\Tenancy\Bootstrappers\MailTenancyBootstrapper::class, // Queueing mail requires using QueueTenancyBootstrapper with $forceRefresh set to true
|
||||||
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
|
||||||
],
|
],
|
||||||
|
|
@ -284,7 +285,6 @@ return [
|
||||||
'features' => [
|
'features' => [
|
||||||
// Stancl\Tenancy\Features\UserImpersonation::class,
|
// Stancl\Tenancy\Features\UserImpersonation::class,
|
||||||
// Stancl\Tenancy\Features\TelescopeTags::class,
|
// Stancl\Tenancy\Features\TelescopeTags::class,
|
||||||
// Stancl\Tenancy\Features\UniversalRoutes::class,
|
|
||||||
// Stancl\Tenancy\Features\TenantConfig::class, // https://tenancyforlaravel.com/docs/v3/features/tenant-config
|
// Stancl\Tenancy\Features\TenantConfig::class, // https://tenancyforlaravel.com/docs/v3/features/tenant-config
|
||||||
// Stancl\Tenancy\Features\CrossDomainRedirect::class, // https://tenancyforlaravel.com/docs/v3/features/cross-domain-redirect
|
// Stancl\Tenancy\Features\CrossDomainRedirect::class, // https://tenancyforlaravel.com/docs/v3/features/cross-domain-redirect
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains;
|
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
@ -21,7 +21,7 @@ use Stancl\Tenancy\Middleware\PreventAccessFromCentralDomains;
|
||||||
Route::middleware([
|
Route::middleware([
|
||||||
'web',
|
'web',
|
||||||
InitializeTenancyByDomain::class,
|
InitializeTenancyByDomain::class,
|
||||||
PreventAccessFromCentralDomains::class,
|
PreventAccessFromUnwantedDomains::class,
|
||||||
])->group(function () {
|
])->group(function () {
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
|
return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
|
||||||
|
|
|
||||||
|
|
@ -15,22 +15,22 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1",
|
"php": "^8.2",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"illuminate/support": "^9.0",
|
"illuminate/support": "^9.38",
|
||||||
"spatie/ignition": "^1.4",
|
"spatie/ignition": "^1.4",
|
||||||
"ramsey/uuid": "^4.0",
|
"ramsey/uuid": "^4.0",
|
||||||
"stancl/jobpipeline": "^1.0",
|
"stancl/jobpipeline": "^1.0",
|
||||||
"stancl/virtualcolumn": "^1.3"
|
"stancl/virtualcolumn": "^1.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"laravel/framework": "dev-cache-methods as 9.21",
|
"laravel/framework": "dev-cache-methods as 9.38",
|
||||||
"orchestra/testbench": "^7.0",
|
"orchestra/testbench": "^7.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"doctrine/dbal": "^2.10",
|
"doctrine/dbal": "^2.10",
|
||||||
"spatie/valuestore": "^1.2.5",
|
"spatie/valuestore": "^1.2.5",
|
||||||
"pestphp/pest": "^1.21",
|
"pestphp/pest": "^1.21",
|
||||||
"nunomaduro/larastan": "^1.0",
|
"nunomaduro/larastan": "^2.4",
|
||||||
"spatie/invade": "^1.1"
|
"spatie/invade": "^1.1"
|
||||||
},
|
},
|
||||||
"repositories": [
|
"repositories": [
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,8 @@ parameters:
|
||||||
paths:
|
paths:
|
||||||
- src/Database/DatabaseConfig.php
|
- src/Database/DatabaseConfig.php
|
||||||
- '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#'
|
- '#Method Stancl\\Tenancy\\Tenancy::cachedResolvers\(\) should return array#'
|
||||||
|
- '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#'
|
||||||
# php 8.2
|
- '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#'
|
||||||
# - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$tenancy#'
|
|
||||||
# - '#Access to an undefined property Stancl\\Tenancy\\Middleware\\IdentificationMiddleware\:\:\$resolver#'
|
|
||||||
|
|
||||||
checkMissingIterableValueType: false
|
checkMissingIterableValueType: false
|
||||||
treatPhpDocTypesAsCertain: false
|
treatPhpDocTypesAsCertain: false
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
</filter>
|
</filter>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<env name="APP_ENV" value="testing"/>
|
||||||
|
<env name="APP_KEY" value="base64:uYVmTs9lrQbXWfHgSSiG0VZMjc2KG/fBbjV1i1JDVos="/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="CACHE_DRIVER" value="redis"/>
|
<env name="CACHE_DRIVER" value="redis"/>
|
||||||
<env name="MAIL_DRIVER" value="array"/>
|
<env name="MAIL_DRIVER" value="array"/>
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,9 @@ class QueueTenancyBootstrapper implements TenancyBootstrapper
|
||||||
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
|
$dispatcher->listen(JobFailed::class, $revertToPreviousState); // artisan('queue:work') which fails
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static function initializeTenancyForQueue(string|int $tenantId): void
|
protected static function initializeTenancyForQueue(string|int|null $tenantId): void
|
||||||
{
|
{
|
||||||
if (! $tenantId) {
|
if ($tenantId === null) {
|
||||||
// The job is not tenant-aware
|
// The job is not tenant-aware
|
||||||
if (tenancy()->initialized) {
|
if (tenancy()->initialized) {
|
||||||
// Tenancy was initialized, so we revert back to the central context
|
// Tenancy was initialized, so we revert back to the central context
|
||||||
|
|
|
||||||
66
src/Bootstrappers/SessionTenancyBootstrapper.php
Normal file
66
src/Bootstrappers/SessionTenancyBootstrapper.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Bootstrappers;
|
||||||
|
|
||||||
|
use Illuminate\Config\Repository;
|
||||||
|
use Illuminate\Contracts\Container\Container;
|
||||||
|
use Illuminate\Session\DatabaseSessionHandler;
|
||||||
|
use Illuminate\Session\SessionManager;
|
||||||
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This resets the database connection used by the database session driver.
|
||||||
|
*
|
||||||
|
* It runs each time tenancy is initialized or ended.
|
||||||
|
* That way the session driver always uses the current DB connection.
|
||||||
|
*/
|
||||||
|
class SessionTenancyBootstrapper implements TenancyBootstrapper
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected Repository $config,
|
||||||
|
protected Container $container,
|
||||||
|
protected SessionManager $session,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bootstrap(Tenant $tenant): void
|
||||||
|
{
|
||||||
|
$this->resetDatabaseHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function revert(): void
|
||||||
|
{
|
||||||
|
// When ending tenancy, this runs *before* the DatabaseTenancyBootstrapper, so DB tenancy
|
||||||
|
// is still bootstrapped. For that reason, we have to explicitly use the central connection
|
||||||
|
$this->resetDatabaseHandler(config('tenancy.database.central_connection'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resetDatabaseHandler(string $defaultConnection = null): void
|
||||||
|
{
|
||||||
|
$sessionDrivers = $this->session->getDrivers();
|
||||||
|
|
||||||
|
if (isset($sessionDrivers['database'])) {
|
||||||
|
/** @var \Illuminate\Session\Store $databaseDriver */
|
||||||
|
$databaseDriver = $sessionDrivers['database'];
|
||||||
|
|
||||||
|
$databaseDriver->setHandler($this->createDatabaseHandler($defaultConnection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createDatabaseHandler(string $defaultConnection = null): DatabaseSessionHandler
|
||||||
|
{
|
||||||
|
// Typically returns null, so this falls back to the default DB connection
|
||||||
|
$connection = $this->config->get('session.connection') ?? $defaultConnection;
|
||||||
|
|
||||||
|
// Based on SessionManager::createDatabaseDriver
|
||||||
|
return new DatabaseSessionHandler(
|
||||||
|
$this->container->make('db')->connection($connection),
|
||||||
|
$this->config->get('session.table'),
|
||||||
|
$this->config->get('session.lifetime'),
|
||||||
|
$this->container,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Concerns;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Stancl\Tenancy\Enums\LogMode;
|
|
||||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
|
|
||||||
// todo finish this feature
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @mixin Tenancy
|
|
||||||
*/
|
|
||||||
trait Debuggable
|
|
||||||
{
|
|
||||||
protected LogMode $logMode = LogMode::NONE;
|
|
||||||
protected array $eventLog = [];
|
|
||||||
|
|
||||||
public function log(LogMode $mode = LogMode::SILENT): static
|
|
||||||
{
|
|
||||||
$this->eventLog = [];
|
|
||||||
$this->logMode = $mode;
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function logMode(): LogMode
|
|
||||||
{
|
|
||||||
return $this->logMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLog(): array
|
|
||||||
{
|
|
||||||
return $this->eventLog;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function logEvent(TenancyEvent $event): static
|
|
||||||
{
|
|
||||||
$this->eventLog[] = ['time' => now(), 'event' => $event::class, 'tenant' => $this->tenant];
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dump(Closure $dump = null): static
|
|
||||||
{
|
|
||||||
$dump ??= dd(...);
|
|
||||||
|
|
||||||
// Dump the log if we were already logging in silent mode
|
|
||||||
// Otherwise start logging in instant mode
|
|
||||||
match ($this->logMode) {
|
|
||||||
LogMode::NONE => $this->log(LogMode::INSTANT),
|
|
||||||
LogMode::SILENT => $dump($this->eventLog),
|
|
||||||
LogMode::INSTANT => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dd(Closure $dump = null): void
|
|
||||||
{
|
|
||||||
$dump ??= dd(...);
|
|
||||||
|
|
||||||
if ($this->logMode === LogMode::SILENT) {
|
|
||||||
$dump($this->eventLog);
|
|
||||||
} else {
|
|
||||||
$dump($this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Database\Concerns;
|
namespace Stancl\Tenancy\Database\Concerns;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Database\TenantScope;
|
use Stancl\Tenancy\Database\TenantScope;
|
||||||
use Stancl\Tenancy\Tenancy;
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
@ -13,7 +14,7 @@ use Stancl\Tenancy\Tenancy;
|
||||||
*/
|
*/
|
||||||
trait BelongsToTenant
|
trait BelongsToTenant
|
||||||
{
|
{
|
||||||
public function tenant()
|
public function tenant(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
|
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ use Stancl\Tenancy\Contracts;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Database\Concerns;
|
use Stancl\Tenancy\Database\Concerns;
|
||||||
use Stancl\Tenancy\Events;
|
use Stancl\Tenancy\Events;
|
||||||
|
use Stancl\Tenancy\Tenancy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $domain
|
* @property string $domain
|
||||||
|
|
@ -28,7 +29,7 @@ class Domain extends Model implements Contracts\Domain
|
||||||
|
|
||||||
public function tenant(): BelongsTo
|
public function tenant(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(config('tenancy.models.tenant'));
|
return $this->belongsTo(config('tenancy.models.tenant'), Tenancy::tenantKeyColumn());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected $dispatchesEvents = [
|
protected $dispatchesEvents = [
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Enums;
|
|
||||||
|
|
||||||
enum LogMode
|
|
||||||
{
|
|
||||||
case NONE;
|
|
||||||
case SILENT;
|
|
||||||
case INSTANT;
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Features;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Routing\Route;
|
|
||||||
use Illuminate\Support\Facades\Route as Router;
|
|
||||||
use Stancl\Tenancy\Contracts\Feature;
|
|
||||||
use Stancl\Tenancy\Middleware;
|
|
||||||
use Stancl\Tenancy\Tenancy;
|
|
||||||
|
|
||||||
class UniversalRoutes implements Feature
|
|
||||||
{
|
|
||||||
public static string $middlewareGroup = 'universal';
|
|
||||||
|
|
||||||
// todo docblock
|
|
||||||
/** @var array<class-string<\Stancl\Tenancy\Middleware\IdentificationMiddleware>> */
|
|
||||||
public static array $identificationMiddlewares = [
|
|
||||||
Middleware\InitializeTenancyByDomain::class,
|
|
||||||
Middleware\InitializeTenancyBySubdomain::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
public function bootstrap(Tenancy $tenancy): void
|
|
||||||
{
|
|
||||||
foreach (static::$identificationMiddlewares as $middleware) {
|
|
||||||
$originalOnFail = $middleware::$onFail;
|
|
||||||
|
|
||||||
$middleware::$onFail = function ($exception, $request, $next) use ($originalOnFail) {
|
|
||||||
if (static::routeHasMiddleware($request->route(), static::$middlewareGroup)) {
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($originalOnFail) {
|
|
||||||
return $originalOnFail($exception, $request, $next);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw $exception;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function routeHasMiddleware(Route $route, string $middleware): bool
|
|
||||||
{
|
|
||||||
/** @var array $routeMiddleware */
|
|
||||||
$routeMiddleware = $route->middleware();
|
|
||||||
|
|
||||||
if (in_array($middleware, $routeMiddleware, true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop one level deep and check if the route's middleware
|
|
||||||
// groups have the searched middleware group inside them
|
|
||||||
$middlewareGroups = Router::getMiddlewareGroups();
|
|
||||||
foreach ($route->gatherMiddleware() as $inner) {
|
|
||||||
if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
18
src/Listeners/CreateTenantStorage.php
Normal file
18
src/Listeners/CreateTenantStorage.php
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Listeners;
|
||||||
|
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
|
||||||
|
class CreateTenantStorage
|
||||||
|
{
|
||||||
|
public function handle(TenantCreated $event): void
|
||||||
|
{
|
||||||
|
$storage_path = $event->tenant->run(fn () => storage_path());
|
||||||
|
|
||||||
|
mkdir("$storage_path", 0777, true); // Create the tenant's folder inside storage/
|
||||||
|
mkdir("$storage_path/framework/cache", 0777, true); // Create /framework/cache inside the tenant's storage (used for e.g. real-time facades)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,9 @@ class DeleteTenantStorage
|
||||||
{
|
{
|
||||||
public function handle(DeletingTenant $event): void
|
public function handle(DeletingTenant $event): void
|
||||||
{
|
{
|
||||||
|
// todo@lukas since this is using the 'File' facade instead of low-level PHP functions, Tenancy might affect this?
|
||||||
|
// Therefore, when Tenancy is initialized, this might look INSIDE the tenant's storage, instead of the main storage dir?
|
||||||
|
// The DeletingTenant event will be fired in the central context in 99% of cases, but sometimes it might run in the tenant context (from another tenant) so we want to make sure this works well in all contexts.
|
||||||
File::deleteDirectory($event->tenant->run(fn () => storage_path()));
|
File::deleteDirectory($event->tenant->run(fn () => storage_path()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ class InitializeTenancyByDomain extends IdentificationMiddleware
|
||||||
/** @return \Illuminate\Http\Response|mixed */
|
/** @return \Illuminate\Http\Response|mixed */
|
||||||
public function handle(Request $request, Closure $next): mixed
|
public function handle(Request $request, Closure $next): mixed
|
||||||
{
|
{
|
||||||
|
if (in_array($request->getHost(), config('tenancy.central_domains', []), true)) {
|
||||||
|
// Always bypass tenancy initialization when host is in central domains
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->initializeTenancy(
|
return $this->initializeTenancy(
|
||||||
$request,
|
$request,
|
||||||
$next,
|
$next,
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,13 @@ class InitializeTenancyByPath extends IdentificationMiddleware
|
||||||
/** @return \Illuminate\Http\Response|mixed */
|
/** @return \Illuminate\Http\Response|mixed */
|
||||||
public function handle(Request $request, Closure $next): mixed
|
public function handle(Request $request, Closure $next): mixed
|
||||||
{
|
{
|
||||||
/** @var Route $route */
|
$route = $this->route($request);
|
||||||
$route = $request->route();
|
|
||||||
|
|
||||||
// Only initialize tenancy if tenant is the first parameter
|
// Only initialize tenancy if tenant is the first parameter
|
||||||
// We don't want to initialize tenancy if the tenant is
|
// We don't want to initialize tenancy if the tenant is
|
||||||
// simply injected into some route controller action.
|
// simply injected into some route controller action.
|
||||||
if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) {
|
if ($route->parameterNames()[0] === PathTenantResolver::tenantParameterName()) {
|
||||||
$this->setDefaultTenantForRouteParametersWhenTenancyIsInitialized();
|
$this->setDefaultTenantForRouteParametersWhenInitializingTenancy();
|
||||||
|
|
||||||
return $this->initializeTenancy(
|
return $this->initializeTenancy(
|
||||||
$request,
|
$request,
|
||||||
|
|
@ -47,7 +46,26 @@ class InitializeTenancyByPath extends IdentificationMiddleware
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function setDefaultTenantForRouteParametersWhenTenancyIsInitialized(): void
|
protected function route(Request $request): Route
|
||||||
|
{
|
||||||
|
/** @var ?Route $route */
|
||||||
|
$route = $request->route();
|
||||||
|
|
||||||
|
if (! $route) {
|
||||||
|
// Create a fake $route instance that has enough information for this middleware's needs
|
||||||
|
$route = new Route($request->method(), $request->getUri(), []);
|
||||||
|
/**
|
||||||
|
* getPathInfo() returns the path except the root domain.
|
||||||
|
* We fetch the first parameter because tenant parameter is *always* first.
|
||||||
|
*/
|
||||||
|
$route->parameters[PathTenantResolver::tenantParameterName()] = explode('/', ltrim($request->getPathInfo(), '/'))[0];
|
||||||
|
$route->parameterNames[] = PathTenantResolver::tenantParameterName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $route;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setDefaultTenantForRouteParametersWhenInitializingTenancy(): void
|
||||||
{
|
{
|
||||||
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
|
Event::listen(InitializingTenancy::class, function (InitializingTenancy $event) {
|
||||||
/** @var Tenant $tenant */
|
/** @var Tenant $tenant */
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,11 @@ class InitializeTenancyBySubdomain extends InitializeTenancyByDomain
|
||||||
/** @return Response|mixed */
|
/** @return Response|mixed */
|
||||||
public function handle(Request $request, Closure $next): mixed
|
public function handle(Request $request, Closure $next): mixed
|
||||||
{
|
{
|
||||||
|
if (in_array($request->getHost(), config('tenancy.central_domains', []), true)) {
|
||||||
|
// Always bypass tenancy initialization when host is in central domains
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
$subdomain = $this->makeSubdomain($request->getHost());
|
$subdomain = $this->makeSubdomain($request->getHost());
|
||||||
|
|
||||||
if (is_object($subdomain) && $subdomain instanceof Exception) {
|
if (is_object($subdomain) && $subdomain instanceof Exception) {
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Stancl\Tenancy\Middleware;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class PreventAccessFromCentralDomains
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Set this property if you want to customize the on-fail behavior.
|
|
||||||
*/
|
|
||||||
public static ?Closure $abortRequest;
|
|
||||||
|
|
||||||
/** @return \Illuminate\Http\Response|mixed */
|
|
||||||
public function handle(Request $request, Closure $next): mixed
|
|
||||||
{
|
|
||||||
if (in_array($request->getHost(), config('tenancy.central_domains'))) {
|
|
||||||
$abortRequest = static::$abortRequest ?? function () {
|
|
||||||
abort(404);
|
|
||||||
};
|
|
||||||
|
|
||||||
return $abortRequest($request, $next);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
61
src/Middleware/PreventAccessFromUnwantedDomains.php
Normal file
61
src/Middleware/PreventAccessFromUnwantedDomains.php
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Routing\Route;
|
||||||
|
use Illuminate\Support\Facades\Route as Router;
|
||||||
|
|
||||||
|
// todo come up with a better name
|
||||||
|
class PreventAccessFromUnwantedDomains
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Set this property if you want to customize the on-fail behavior.
|
||||||
|
*/
|
||||||
|
public static ?Closure $abortRequest;
|
||||||
|
|
||||||
|
/** @return \Illuminate\Http\Response|mixed */
|
||||||
|
public function handle(Request $request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
/** @var Route $route */
|
||||||
|
$route = $request->route();
|
||||||
|
|
||||||
|
if ($this->routeHasMiddleware($route, 'universal')) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($request->getHost(), config('tenancy.central_domains'), true)) {
|
||||||
|
$abortRequest = static::$abortRequest ?? function () {
|
||||||
|
abort(404);
|
||||||
|
};
|
||||||
|
|
||||||
|
return $abortRequest($request, $next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function routeHasMiddleware(Route $route, string $middleware): bool
|
||||||
|
{
|
||||||
|
/** @var array $routeMiddleware */
|
||||||
|
$routeMiddleware = $route->middleware();
|
||||||
|
|
||||||
|
if (in_array($middleware, $routeMiddleware, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop one level deep and check if the route's middleware
|
||||||
|
// groups have the searched middleware group inside them
|
||||||
|
$middlewareGroups = Router::getMiddlewareGroups();
|
||||||
|
foreach ($route->gatherMiddleware() as $inner) {
|
||||||
|
if (! $inner instanceof Closure && isset($middlewareGroups[$inner]) && in_array($middleware, $middlewareGroups[$inner], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,9 +27,7 @@ abstract class CachedTenantResolver implements TenantResolver
|
||||||
|
|
||||||
$key = $this->getCacheKey(...$args);
|
$key = $this->getCacheKey(...$args);
|
||||||
|
|
||||||
if ($this->cache->has($key)) {
|
if ($tenant = $this->cache->get($key)) {
|
||||||
$tenant = $this->cache->get($key);
|
|
||||||
|
|
||||||
$this->resolved($tenant, ...$args);
|
$this->resolved($tenant, ...$args);
|
||||||
|
|
||||||
return $tenant;
|
return $tenant;
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,18 @@ use Closure;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Traits\Macroable;
|
use Illuminate\Support\Traits\Macroable;
|
||||||
use Stancl\Tenancy\Concerns\Debuggable;
|
|
||||||
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
use Stancl\Tenancy\Contracts\TenancyBootstrapper;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedByIdException;
|
||||||
|
|
||||||
class Tenancy
|
class Tenancy
|
||||||
{
|
{
|
||||||
use Macroable, Debuggable;
|
use Macroable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current tenant.
|
* The current tenant.
|
||||||
*
|
|
||||||
* @var (Tenant&Model)|null
|
|
||||||
*/
|
*/
|
||||||
public ?Tenant $tenant = null;
|
public (Tenant&Model)|null $tenant = null;
|
||||||
|
|
||||||
// todo docblock
|
// todo docblock
|
||||||
public ?Closure $getBootstrappersUsing = null;
|
public ?Closure $getBootstrappersUsing = null;
|
||||||
|
|
@ -97,9 +94,9 @@ class Tenancy
|
||||||
|
|
||||||
public static function model(): Tenant&Model
|
public static function model(): Tenant&Model
|
||||||
{
|
{
|
||||||
|
/** @var class-string<Tenant&Model> $class */
|
||||||
$class = config('tenancy.models.tenant');
|
$class = config('tenancy.models.tenant');
|
||||||
|
|
||||||
/** @var Tenant&Model $model */
|
|
||||||
$model = new $class;
|
$model = new $class;
|
||||||
|
|
||||||
return $model;
|
return $model;
|
||||||
|
|
@ -113,13 +110,9 @@ class Tenancy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to find a tenant using an ID.
|
* Try to find a tenant using an ID.
|
||||||
*
|
|
||||||
* @return (Tenant&Model)|null
|
|
||||||
*/
|
*/
|
||||||
public static function find(int|string $id): Tenant|null
|
public static function find(int|string $id): (Tenant&Model)|null
|
||||||
{
|
{
|
||||||
// todo update all syntax like this once we're fully on PHP 8.2
|
|
||||||
/** @var (Tenant&Model)|null */
|
|
||||||
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();
|
$tenant = static::model()->where(static::model()->getTenantKeyName(), $id)->first();
|
||||||
|
|
||||||
return $tenant;
|
return $tenant;
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,11 @@ namespace Stancl\Tenancy;
|
||||||
|
|
||||||
use Illuminate\Cache\CacheManager;
|
use Illuminate\Cache\CacheManager;
|
||||||
use Illuminate\Database\Console\Migrations\FreshCommand;
|
use Illuminate\Database\Console\Migrations\FreshCommand;
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper;
|
||||||
use Stancl\Tenancy\CacheManager as TenantCacheManager;
|
use Stancl\Tenancy\CacheManager as TenantCacheManager;
|
||||||
use Stancl\Tenancy\Contracts\Domain;
|
use Stancl\Tenancy\Contracts\Domain;
|
||||||
use Stancl\Tenancy\Contracts\Tenant;
|
use Stancl\Tenancy\Contracts\Tenant;
|
||||||
use Stancl\Tenancy\Enums\LogMode;
|
|
||||||
use Stancl\Tenancy\Events\Contracts\TenancyEvent;
|
|
||||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
|
||||||
class TenancyServiceProvider extends ServiceProvider
|
class TenancyServiceProvider extends ServiceProvider
|
||||||
|
|
@ -126,18 +123,6 @@ class TenancyServiceProvider extends ServiceProvider
|
||||||
$this->loadRoutesFrom(__DIR__ . '/../assets/routes.php');
|
$this->loadRoutesFrom(__DIR__ . '/../assets/routes.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::listen('Stancl\\Tenancy\\Events\\*', function (string $name, array $data) {
|
|
||||||
$event = $data[0];
|
|
||||||
|
|
||||||
if ($event instanceof TenancyEvent) {
|
|
||||||
match (tenancy()->logMode()) {
|
|
||||||
LogMode::SILENT => tenancy()->logEvent($event),
|
|
||||||
LogMode::INSTANT => dump($event), // todo1 perhaps still log
|
|
||||||
default => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->app->singleton('globalUrl', function ($app) {
|
$this->app->singleton('globalUrl', function ($app) {
|
||||||
if ($app->bound(FilesystemTenancyBootstrapper::class)) {
|
if ($app->bound(FilesystemTenancyBootstrapper::class)) {
|
||||||
$instance = clone $app['url'];
|
$instance = clone $app['url'];
|
||||||
|
|
|
||||||
|
|
@ -180,17 +180,17 @@ test('rollback command works', function () {
|
||||||
expect(Schema::hasTable('users'))->toBeFalse();
|
expect(Schema::hasTable('users'))->toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('seed command works', function (){
|
test('seed command works', function () {
|
||||||
$tenant = Tenant::create();
|
$tenant = Tenant::create();
|
||||||
Artisan::call('tenants:migrate');
|
Artisan::call('tenants:migrate');
|
||||||
|
|
||||||
$tenant->run(function (){
|
$tenant->run(function () {
|
||||||
expect(DB::table('users')->count())->toBe(0);
|
expect(DB::table('users')->count())->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
Artisan::call('tenants:seed', ['--class' => TestSeeder::class]);
|
Artisan::call('tenants:seed', ['--class' => TestSeeder::class]);
|
||||||
|
|
||||||
$tenant->run(function (){
|
$tenant->run(function () {
|
||||||
$user = DB::table('users');
|
$user = DB::table('users');
|
||||||
expect($user->count())->toBe(1)
|
expect($user->count())->toBe(1)
|
||||||
->and($user->first()->email)->toBe('seeded@user');
|
->and($user->first()->email)->toBe('seeded@user');
|
||||||
|
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Stancl\Tenancy\Enums\LogMode;
|
|
||||||
use Stancl\Tenancy\Events\EndingTenancy;
|
|
||||||
use Stancl\Tenancy\Events\InitializingTenancy;
|
|
||||||
use Stancl\Tenancy\Events\TenancyEnded;
|
|
||||||
use Stancl\Tenancy\Events\TenancyInitialized;
|
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
|
||||||
|
|
||||||
test('tenancy can log events silently', function () {
|
|
||||||
tenancy()->log(LogMode::SILENT);
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
assertTenancyInitializedAndEnded(tenancy()->getLog(), $tenant);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('tenancy logs event silently by default', function () {
|
|
||||||
tenancy()->log();
|
|
||||||
|
|
||||||
expect(tenancy()->logMode())->toBe(LogMode::SILENT);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('the log can be dumped', function (string $method) {
|
|
||||||
tenancy()->log();
|
|
||||||
|
|
||||||
$tenant = Tenant::create();
|
|
||||||
|
|
||||||
tenancy()->initialize($tenant);
|
|
||||||
|
|
||||||
tenancy()->end();
|
|
||||||
|
|
||||||
$output = [];
|
|
||||||
tenancy()->$method(function ($data) use (&$output) {
|
|
||||||
$output = $data;
|
|
||||||
});
|
|
||||||
|
|
||||||
assertTenancyInitializedAndEnded($output, $tenant);
|
|
||||||
})->with([
|
|
||||||
'dump',
|
|
||||||
'dd',
|
|
||||||
]);
|
|
||||||
|
|
||||||
test('tenancy can log events immediately', function () {
|
|
||||||
// todo implement
|
|
||||||
pest()->markTestIncomplete();
|
|
||||||
});
|
|
||||||
|
|
||||||
// todo test the different behavior of the methods in different contexts, or get rid of the logic and simplify it
|
|
||||||
|
|
||||||
function assertTenancyInitializedAndEnded(array $log, Tenant $tenant): void
|
|
||||||
{
|
|
||||||
expect($log)->toHaveCount(4);
|
|
||||||
|
|
||||||
expect($log[0]['event'])->toBe(InitializingTenancy::class);
|
|
||||||
expect($log[0]['tenant'])->toBe($tenant);
|
|
||||||
expect($log[1]['event'])->toBe(TenancyInitialized::class);
|
|
||||||
expect($log[1]['tenant'])->toBe($tenant);
|
|
||||||
|
|
||||||
expect($log[2]['event'])->toBe(EndingTenancy::class);
|
|
||||||
expect($log[2]['tenant'])->toBe($tenant);
|
|
||||||
expect($log[3]['event'])->toBe(TenancyEnded::class);
|
|
||||||
expect($log[3]['tenant'])->toBe($tenant);
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,7 @@ beforeEach(function () {
|
||||||
config(['tenancy.models.tenant' => DatabaseAndDomainTenant::class]);
|
config(['tenancy.models.tenant' => DatabaseAndDomainTenant::class]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('job delete domains successfully', function (){
|
test('job delete domains successfully', function () {
|
||||||
$tenant = DatabaseAndDomainTenant::create();
|
$tenant = DatabaseAndDomainTenant::create();
|
||||||
|
|
||||||
$tenant->domains()->create([
|
$tenant->domains()->create([
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ use Stancl\Tenancy\Database\Models;
|
||||||
use Stancl\Tenancy\Database\Models\Domain;
|
use Stancl\Tenancy\Database\Models\Domain;
|
||||||
use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException;
|
use Stancl\Tenancy\Exceptions\DomainOccupiedByOtherTenantException;
|
||||||
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
use Stancl\Tenancy\Features\UniversalRoutes;
|
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
|
||||||
|
|
||||||
|
|
@ -95,7 +94,6 @@ test('throw correct exception when onFail is null and universal routes are enabl
|
||||||
|
|
||||||
// Enable UniversalRoute feature
|
// Enable UniversalRoute feature
|
||||||
Route::middlewareGroup('universal', []);
|
Route::middlewareGroup('universal', []);
|
||||||
config(['tenancy.features' => [UniversalRoutes::class]]);
|
|
||||||
|
|
||||||
$this->withoutExceptionHandling()->get('http://foo.localhost/foo/abc/xyz');
|
$this->withoutExceptionHandling()->get('http://foo.localhost/foo/abc/xyz');
|
||||||
})->throws(TenantCouldNotBeIdentifiedOnDomainException::class);;
|
})->throws(TenantCouldNotBeIdentifiedOnDomainException::class);;
|
||||||
|
|
|
||||||
104
tests/EarlyIdentificationTest.php
Normal file
104
tests/EarlyIdentificationTest.php
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Http\Kernel;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Stancl\Tenancy\Events\TenancyInitialized;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByPath;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByRequestData;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain;
|
||||||
|
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\EarlyIdentification\Controller;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config()->set([
|
||||||
|
'tenancy.token' => 'central-abc123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Event::listen(TenancyInitialized::class, function (TenancyInitialized $event) {
|
||||||
|
config()->set([
|
||||||
|
'tenancy.token' => $event->tenancy->tenant->getTenantKey() . '-abc123',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('early identification works with path identification', function () {
|
||||||
|
app(Kernel::class)->pushMiddleware(InitializeTenancyByPath::class);
|
||||||
|
|
||||||
|
Route::group([
|
||||||
|
'prefix' => '/{tenant}',
|
||||||
|
], function () {
|
||||||
|
Route::get('/foo', [Controller::class, 'index'])->name('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
Tenant::create([
|
||||||
|
'id' => 'acme',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = pest()->get('/acme/foo')->assertOk();
|
||||||
|
|
||||||
|
assertTenancyInitializedInEarlyIdentificationRequest($response->getContent());
|
||||||
|
|
||||||
|
// check if default parameter feature is working fine by asserting that the route WITHOUT the tenant parameter
|
||||||
|
// matches the route WITH the tenant parameter
|
||||||
|
expect(route('foo'))->toBe(route('foo', ['tenant' => 'acme']));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('early identification works with request data identification', function (string $type) {
|
||||||
|
app(Kernel::class)->pushMiddleware(InitializeTenancyByRequestData::class);
|
||||||
|
|
||||||
|
Route::get('/foo', [Controller::class, 'index'])->name('foo');
|
||||||
|
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'id' => 'acme',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($type === 'header') {
|
||||||
|
$response = pest()->get('/foo', ['X-Tenant' => $tenant->id])->assertOk();
|
||||||
|
} elseif ($type === 'queryParameter') {
|
||||||
|
$response = pest()->get("/foo?tenant=$tenant->id")->assertOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTenancyInitializedInEarlyIdentificationRequest($response->getContent());
|
||||||
|
})->with([
|
||||||
|
'using request header parameter' => 'header',
|
||||||
|
'using request query parameter' => 'queryParameter'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// The name of this test is suffixed by the dataset — domain / subdomain / domainOrSubdomain identification
|
||||||
|
test('early identification works', function (string $middleware, string $domain, string $url) {
|
||||||
|
app(Kernel::class)->pushMiddleware($middleware);
|
||||||
|
|
||||||
|
config(['tenancy.tenant_model' => Tenant::class]);
|
||||||
|
|
||||||
|
Route::get('/foo', [Controller::class, 'index'])
|
||||||
|
->middleware(PreventAccessFromUnwantedDomains::class)
|
||||||
|
->name('foo');
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
$tenant->domains()->create([
|
||||||
|
'domain' => $domain,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = pest()->get($url)->assertOk();
|
||||||
|
|
||||||
|
assertTenancyInitializedInEarlyIdentificationRequest($response->getContent());
|
||||||
|
})->with([
|
||||||
|
'domain identification' => ['middleware' => InitializeTenancyByDomain::class, 'domain' => 'foo.test', 'url' => 'http://foo.test/foo'],
|
||||||
|
'subdomain identification' => ['middleware' => InitializeTenancyBySubdomain::class, 'domain' => 'foo', 'url' => 'http://foo.localhost/foo'],
|
||||||
|
'domainOrSubdomain identification using domain' => ['middleware' => InitializeTenancyByDomainOrSubdomain::class, 'domain' => 'foo.test', 'url' => 'http://foo.test/foo'],
|
||||||
|
'domainOrSubdomain identification using subdomain' => ['middleware' => InitializeTenancyByDomainOrSubdomain::class, 'domain' => 'foo', 'url' => 'http://foo.localhost/foo'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function assertTenancyInitializedInEarlyIdentificationRequest(string|false $string): void
|
||||||
|
{
|
||||||
|
expect($string)->toBe(tenant()->getTenantKey() . '-abc123'); // Assert that the service class returns tenant value
|
||||||
|
expect(app()->make('additionalMiddlewareRunsInTenantContext'))->toBeTrue(); // Assert that middleware added in the controller constructor runs in tenant context
|
||||||
|
expect(app()->make('controllerRunsInTenantContext'))->toBeTrue(); // Assert that tenancy is initialized in the controller constructor
|
||||||
|
}
|
||||||
16
tests/Etc/EarlyIdentification/AdditionalMiddleware.php
Normal file
16
tests/Etc/EarlyIdentification/AdditionalMiddleware.php
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Tests\Etc\EarlyIdentification;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class AdditionalMiddleware
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): mixed
|
||||||
|
{
|
||||||
|
app()->instance('additionalMiddlewareRunsInTenantContext', tenancy()->initialized);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
tests/Etc/EarlyIdentification/Controller.php
Normal file
19
tests/Etc/EarlyIdentification/Controller.php
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Tests\Etc\EarlyIdentification;
|
||||||
|
|
||||||
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
|
||||||
|
class Controller extends BaseController
|
||||||
|
{
|
||||||
|
public function __construct(public Service $service)
|
||||||
|
{
|
||||||
|
app()->instance('controllerRunsInTenantContext', tenancy()->initialized);
|
||||||
|
$this->middleware(AdditionalMiddleware::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): string
|
||||||
|
{
|
||||||
|
return $this->service->token;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
tests/Etc/EarlyIdentification/Service.php
Normal file
15
tests/Etc/EarlyIdentification/Service.php
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Stancl\Tenancy\Tests\Etc\EarlyIdentification;
|
||||||
|
|
||||||
|
class Service
|
||||||
|
{
|
||||||
|
public string $token;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->token = config('tenancy.token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ class HttpKernel extends Kernel
|
||||||
*/
|
*/
|
||||||
protected $middlewareGroups = [
|
protected $middlewareGroups = [
|
||||||
'web' => [
|
'web' => [
|
||||||
\Orchestra\Testbench\Http\Middleware\EncryptCookies::class,
|
\Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
\Illuminate\Session\Middleware\StartSession::class,
|
||||||
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
// \Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateSessionsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('sessions', function (Blueprint $table) {
|
||||||
|
$table->string('id')->primary();
|
||||||
|
$table->foreignId('user_id')->nullable()->index();
|
||||||
|
$table->string('ip_address', 45)->nullable();
|
||||||
|
$table->text('user_agent')->nullable();
|
||||||
|
$table->text('payload');
|
||||||
|
$table->integer('last_activity')->index();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('sessions');
|
||||||
|
}
|
||||||
|
}
|
||||||
145
tests/SessionBootstrapperTest.php
Normal file
145
tests/SessionBootstrapperTest.php
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use Stancl\JobPipeline\JobPipeline;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Bootstrappers\SessionTenancyBootstrapper;
|
||||||
|
use Stancl\Tenancy\Events;
|
||||||
|
use Stancl\Tenancy\Events\TenantCreated;
|
||||||
|
use Stancl\Tenancy\Jobs\CreateDatabase;
|
||||||
|
use Stancl\Tenancy\Listeners;
|
||||||
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This collection of regression tests verifies that SessionTenancyBootstrapper
|
||||||
|
* fully fixes the issue described here https://github.com/archtechx/tenancy/issues/547
|
||||||
|
*
|
||||||
|
* This means: using the DB session driver and:
|
||||||
|
* 1) switching to the central context from tenant requests, OR
|
||||||
|
* 2) switching to the tenant context from central requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
config(['session.driver' => 'database']);
|
||||||
|
config(['tenancy.bootstrappers' => [DatabaseTenancyBootstrapper::class]]);
|
||||||
|
|
||||||
|
Event::listen(
|
||||||
|
TenantCreated::class,
|
||||||
|
JobPipeline::make([CreateDatabase::class])->send(function (TenantCreated $event) {
|
||||||
|
return $event->tenant;
|
||||||
|
})->toListener()
|
||||||
|
);
|
||||||
|
|
||||||
|
Event::listen(Events\TenancyInitialized::class, Listeners\BootstrapTenancy::class);
|
||||||
|
Event::listen(Events\TenancyEnded::class, Listeners\RevertToCentralContext::class);
|
||||||
|
|
||||||
|
// Sessions table for central database
|
||||||
|
pest()->artisan('migrate', [
|
||||||
|
'--path' => __DIR__ . '/Etc/session_migrations',
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('central helper can be used in tenant requests', function (bool $enabled, bool $shouldThrow) {
|
||||||
|
if ($enabled) {
|
||||||
|
config()->set(
|
||||||
|
'tenancy.bootstrappers',
|
||||||
|
array_merge(config('tenancy.bootstrappers'), [SessionTenancyBootstrapper::class]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = Tenant::create();
|
||||||
|
|
||||||
|
$tenant->domains()->create(['domain' => 'foo.localhost']);
|
||||||
|
|
||||||
|
// run for tenants
|
||||||
|
pest()->artisan('tenants:migrate', [
|
||||||
|
'--path' => __DIR__ . '/Etc/session_migrations',
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
Route::middleware(['web', InitializeTenancyByDomain::class])->get('/bar', function () {
|
||||||
|
session(['message' => 'tenant session']);
|
||||||
|
|
||||||
|
tenancy()->central(function () {
|
||||||
|
return 'central results';
|
||||||
|
});
|
||||||
|
|
||||||
|
return session('message');
|
||||||
|
});
|
||||||
|
|
||||||
|
// We initialize tenancy before making the request, since sessions work a bit differently in tests
|
||||||
|
// and we need the DB session handler to use the tenant connection (as it does in a real app on tenant requests).
|
||||||
|
tenancy()->initialize($tenant);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->withoutExceptionHandling()
|
||||||
|
->get('http://foo.localhost/bar')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('tenant session');
|
||||||
|
|
||||||
|
if ($shouldThrow) {
|
||||||
|
pest()->fail('Exception not thrown');
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($shouldThrow) {
|
||||||
|
pest()->assertTrue(true); // empty assertion to make the test pass
|
||||||
|
} else {
|
||||||
|
pest()->fail('Exception thrown: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})->with([
|
||||||
|
['enabled' => false, 'shouldThrow' => true],
|
||||||
|
['enabled' => true, 'shouldThrow' => false],
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('tenant run helper can be used on central requests', function (bool $enabled, bool $shouldThrow) {
|
||||||
|
if ($enabled) {
|
||||||
|
config()->set(
|
||||||
|
'tenancy.bootstrappers',
|
||||||
|
array_merge(config('tenancy.bootstrappers'), [SessionTenancyBootstrapper::class]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tenant::create();
|
||||||
|
|
||||||
|
// run for tenants
|
||||||
|
pest()->artisan('tenants:migrate', [
|
||||||
|
'--path' => __DIR__ . '/Etc/session_migrations',
|
||||||
|
'--realpath' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
|
Route::middleware(['web'])->get('/bar', function () {
|
||||||
|
session(['message' => 'central session']);
|
||||||
|
|
||||||
|
Tenant::first()->run(function () {
|
||||||
|
return 'tenant results';
|
||||||
|
});
|
||||||
|
|
||||||
|
return session('message');
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->withoutExceptionHandling()
|
||||||
|
->get('http://localhost/bar')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('central session');
|
||||||
|
|
||||||
|
if ($shouldThrow) {
|
||||||
|
pest()->fail('Exception not thrown');
|
||||||
|
}
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
if ($shouldThrow) {
|
||||||
|
pest()->assertTrue(true); // empty assertion to make the test pass
|
||||||
|
} else {
|
||||||
|
pest()->fail('Exception thrown: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})->with([
|
||||||
|
['enabled' => false, 'shouldThrow' => true],
|
||||||
|
['enabled' => true, 'shouldThrow' => false],
|
||||||
|
]);
|
||||||
|
|
@ -52,12 +52,13 @@ test('onfail logic can be customized', function () {
|
||||||
->assertSee('foo');
|
->assertSee('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('localhost is not a valid subdomain', function () {
|
test('archte.ch is not a valid subdomain', function () {
|
||||||
pest()->expectException(NotASubdomainException::class);
|
pest()->expectException(NotASubdomainException::class);
|
||||||
|
|
||||||
|
// This gets routed to the app, but with a request domain of 'archte.ch'
|
||||||
$this
|
$this
|
||||||
->withoutExceptionHandling()
|
->withoutExceptionHandling()
|
||||||
->get('http://localhost/foo/abc/xyz');
|
->get('http://archte.ch/foo/abc/xyz');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ip address is not a valid subdomain', function () {
|
test('ip address is not a valid subdomain', function () {
|
||||||
|
|
@ -65,7 +66,7 @@ test('ip address is not a valid subdomain', function () {
|
||||||
|
|
||||||
$this
|
$this
|
||||||
->withoutExceptionHandling()
|
->withoutExceptionHandling()
|
||||||
->get('http://127.0.0.1/foo/abc/xyz');
|
->get('http://127.0.0.2/foo/abc/xyz');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('oninvalidsubdomain logic can be customized', function () {
|
test('oninvalidsubdomain logic can be customized', function () {
|
||||||
|
|
@ -81,7 +82,7 @@ test('oninvalidsubdomain logic can be customized', function () {
|
||||||
|
|
||||||
$this
|
$this
|
||||||
->withoutExceptionHandling()
|
->withoutExceptionHandling()
|
||||||
->get('http://127.0.0.1/foo/abc/xyz')
|
->get('http://127.0.0.2/foo/abc/xyz')
|
||||||
->assertSee('foo custom invalid subdomain handler');
|
->assertSee('foo custom invalid subdomain handler');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -106,26 +107,6 @@ test('we cant use a subdomain that doesnt belong to our central domains', functi
|
||||||
->get('http://foo.localhost/foo/abc/xyz');
|
->get('http://foo.localhost/foo/abc/xyz');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('central domain is not a subdomain', function () {
|
|
||||||
config(['tenancy.central_domains' => [
|
|
||||||
'localhost',
|
|
||||||
]]);
|
|
||||||
|
|
||||||
$tenant = SubdomainTenant::create([
|
|
||||||
'id' => 'acme',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant->domains()->create([
|
|
||||||
'domain' => 'acme',
|
|
||||||
]);
|
|
||||||
|
|
||||||
pest()->expectException(NotASubdomainException::class);
|
|
||||||
|
|
||||||
$this
|
|
||||||
->withoutExceptionHandling()
|
|
||||||
->get('http://localhost/foo/abc/xyz');
|
|
||||||
});
|
|
||||||
|
|
||||||
class SubdomainTenant extends Models\Tenant
|
class SubdomainTenant extends Models\Tenant
|
||||||
{
|
{
|
||||||
use HasDomains;
|
use HasDomains;
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,24 @@
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Stancl\Tenancy\Features\UniversalRoutes;
|
use Stancl\Tenancy\Exceptions\TenantCouldNotBeIdentifiedOnDomainException;
|
||||||
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
|
||||||
|
use Stancl\Tenancy\Middleware\PreventAccessFromUnwantedDomains;
|
||||||
use Stancl\Tenancy\Tests\Etc\Tenant;
|
use Stancl\Tenancy\Tests\Etc\Tenant;
|
||||||
|
use Illuminate\Contracts\Http\Kernel;
|
||||||
|
|
||||||
afterEach(function () {
|
test('a route can work in both central and tenant context', function (array $routeMiddleware, string|null $globalMiddleware) {
|
||||||
InitializeTenancyByDomain::$onFail = null;
|
if ($globalMiddleware) {
|
||||||
});
|
app(Kernel::class)->pushMiddleware($globalMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
test('a route can work in both central and tenant context', function () {
|
|
||||||
Route::middlewareGroup('universal', []);
|
Route::middlewareGroup('universal', []);
|
||||||
config(['tenancy.features' => [UniversalRoutes::class]]);
|
|
||||||
|
|
||||||
Route::get('/foo', function () {
|
Route::get('/foo', function () {
|
||||||
return tenancy()->initialized
|
return tenancy()->initialized
|
||||||
? 'Tenancy is initialized.'
|
? 'Tenancy is initialized.'
|
||||||
: 'Tenancy is not initialized.';
|
: 'Tenancy is not initialized.';
|
||||||
})->middleware(['universal', InitializeTenancyByDomain::class]);
|
})->middleware($routeMiddleware);
|
||||||
|
|
||||||
pest()->get('http://localhost/foo')
|
|
||||||
->assertSuccessful()
|
|
||||||
->assertSee('Tenancy is not initialized.');
|
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'id' => 'acme',
|
'id' => 'acme',
|
||||||
|
|
@ -32,28 +29,33 @@ test('a route can work in both central and tenant context', function () {
|
||||||
'domain' => 'acme.localhost',
|
'domain' => 'acme.localhost',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
pest()->get('http://acme.localhost/foo')
|
pest()->get("http://localhost/foo")
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Tenancy is not initialized.');
|
||||||
|
|
||||||
|
pest()->get("http://acme.localhost/foo")
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Tenancy is initialized.');
|
->assertSee('Tenancy is initialized.');
|
||||||
});
|
})->with('identification types');
|
||||||
|
|
||||||
test('making one route universal doesnt make all routes universal', function () {
|
test('making one route universal doesnt make all routes universal', function (array $routeMiddleware, string|null $globalMiddleware) {
|
||||||
Route::get('/bar', function () {
|
if ($globalMiddleware) {
|
||||||
return tenant('id');
|
app(Kernel::class)->pushMiddleware($globalMiddleware);
|
||||||
})->middleware(InitializeTenancyByDomain::class);
|
}
|
||||||
|
|
||||||
Route::middlewareGroup('universal', []);
|
Route::middlewareGroup('universal', []);
|
||||||
config(['tenancy.features' => [UniversalRoutes::class]]);
|
|
||||||
|
|
||||||
Route::get('/foo', function () {
|
Route::middleware($routeMiddleware)->group(function () {
|
||||||
return tenancy()->initialized
|
Route::get('/nonuniversal', function () {
|
||||||
? 'Tenancy is initialized.'
|
return tenant('id');
|
||||||
: 'Tenancy is not initialized.';
|
});
|
||||||
})->middleware(['universal', InitializeTenancyByDomain::class]);
|
|
||||||
|
|
||||||
pest()->get('http://localhost/foo')
|
Route::get('/universal', function () {
|
||||||
->assertSuccessful()
|
return tenancy()->initialized
|
||||||
->assertSee('Tenancy is not initialized.');
|
? 'Tenancy is initialized.'
|
||||||
|
: 'Tenancy is not initialized.';
|
||||||
|
})->middleware('universal');
|
||||||
|
});
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'id' => 'acme',
|
'id' => 'acme',
|
||||||
|
|
@ -62,16 +64,57 @@ test('making one route universal doesnt make all routes universal', function ()
|
||||||
'domain' => 'acme.localhost',
|
'domain' => 'acme.localhost',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
pest()->get('http://acme.localhost/foo')
|
pest()->get("http://localhost/universal")
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Tenancy is not initialized.');
|
||||||
|
|
||||||
|
pest()->get("http://acme.localhost/universal")
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Tenancy is initialized.');
|
->assertSee('Tenancy is initialized.');
|
||||||
|
|
||||||
tenancy()->end();
|
tenancy()->end();
|
||||||
|
|
||||||
pest()->get('http://localhost/bar')
|
pest()->get('http://localhost/nonuniversal')
|
||||||
->assertStatus(500);
|
->assertStatus(404);
|
||||||
|
|
||||||
pest()->get('http://acme.localhost/bar')
|
pest()->get('http://acme.localhost/nonuniversal')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('acme');
|
->assertSee('acme');
|
||||||
});
|
})->with([
|
||||||
|
'early identification' => [
|
||||||
|
'route_middleware' => [PreventAccessFromUnwantedDomains::class],
|
||||||
|
'global_middleware' => InitializeTenancyByDomain::class,
|
||||||
|
],
|
||||||
|
'route-level identification' => [
|
||||||
|
'route_middleware' => [PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomain::class],
|
||||||
|
'global_middleware' => null,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('it throws correct exception when route is universal and tenant does not exist', function (array $routeMiddleware, string|null $globalMiddleware) {
|
||||||
|
if ($globalMiddleware) {
|
||||||
|
app(Kernel::class)->pushMiddleware($globalMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
Route::middlewareGroup('universal', []);
|
||||||
|
|
||||||
|
Route::get('/foo', function () {
|
||||||
|
return tenancy()->initialized
|
||||||
|
? 'Tenancy is initialized.'
|
||||||
|
: 'Tenancy is not initialized.';
|
||||||
|
})->middleware($routeMiddleware);
|
||||||
|
|
||||||
|
pest()->expectException(TenantCouldNotBeIdentifiedOnDomainException::class);
|
||||||
|
$this->withoutExceptionHandling()->get('http://acme.localhost/foo');
|
||||||
|
})->with('identification types');
|
||||||
|
|
||||||
|
dataset('identification types', [
|
||||||
|
'early identification' => [
|
||||||
|
'route_middleware' => ['universal', PreventAccessFromUnwantedDomains::class],
|
||||||
|
'global_middleware' => InitializeTenancyByDomain::class,
|
||||||
|
],
|
||||||
|
'route-level identification' => [
|
||||||
|
'route_middleware' => ['universal', PreventAccessFromUnwantedDomains::class, InitializeTenancyByDomain::class],
|
||||||
|
'global_middleware' => null,
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue