1
0
Fork 0
mirror of https://github.com/archtechx/tenancy.git synced 2026-02-05 16:54:05 +00:00

Merge branch 'master' into stein-j-readied-tenant

This commit is contained in:
Samuel Štancl 2022-09-29 16:01:24 +02:00 committed by GitHub
commit 0d1a85005d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 180 additions and 52 deletions

View file

@ -10,6 +10,7 @@ $rules = [
'operators' => [ 'operators' => [
'=>' => null, '=>' => null,
'|' => 'no_space', '|' => 'no_space',
'&' => 'no_space',
] ]
], ],
'blank_line_after_namespace' => true, 'blank_line_after_namespace' => true,

View file

@ -63,6 +63,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",
"coverage": "open coverage/phpunit/html/index.html", "coverage": "open coverage/phpunit/html/index.html",
"phpstan": "vendor/bin/phpstan", "phpstan": "vendor/bin/phpstan",
"cs": "php-cs-fixer fix --config=.php-cs-fixer.php",
"test": "PHP_VERSION=8.1 ./test --no-coverage", "test": "PHP_VERSION=8.1 ./test --no-coverage",
"test-full": "PHP_VERSION=8.1 ./test" "test-full": "PHP_VERSION=8.1 ./test"
}, },

View file

@ -8,7 +8,7 @@ use Exception;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection; use Illuminate\Support\LazyCollection;
use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks; use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks;
use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Events\CreatingStorageSymlink; use Stancl\Tenancy\Events\CreatingStorageSymlink;
use Stancl\Tenancy\Events\StorageSymlinkCreated; use Stancl\Tenancy\Events\StorageSymlinkCreated;

View file

@ -7,7 +7,7 @@ namespace Stancl\Tenancy\Actions;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\LazyCollection; use Illuminate\Support\LazyCollection;
use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks; use Stancl\Tenancy\Concerns\DealsWithTenantSymlinks;
use Stancl\Tenancy\Database\Models\Tenant; use Stancl\Tenancy\Contracts\Tenant;
use Stancl\Tenancy\Events\RemovingStorageSymlink; use Stancl\Tenancy\Events\RemovingStorageSymlink;
use Stancl\Tenancy\Events\StorageSymlinkRemoved; use Stancl\Tenancy\Events\StorageSymlinkRemoved;

52
src/Commands/Down.php Normal file
View file

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Commands;
use Illuminate\Foundation\Console\DownCommand;
use Stancl\Tenancy\Concerns\HasATenantsOption;
class Down extends DownCommand
{
use HasATenantsOption;
protected $signature = 'tenants:down
{--redirect= : The path that users should be redirected to}
{--retry= : The number of seconds after which the request may be retried}
{--refresh= : The number of seconds after which the browser may refresh}
{--secret= : The secret phrase that may be used to bypass maintenance mode}
{--status=503 : The status code that should be used when returning the maintenance mode response}';
protected $description = 'Put tenants into maintenance mode.';
public function handle(): void
{
// The base down command is heavily used. Instead of saving the data inside a file,
// the data is stored the tenant database, which means some Laravel features
// are not available with tenants.
$payload = $this->getDownDatabasePayload();
// This runs for all tenants if no --tenants are specified
tenancy()->runForMultiple($this->option('tenants'), function ($tenant) use ($payload) {
$this->line("Tenant: {$tenant['id']}");
$tenant->putDownForMaintenance($payload);
});
$this->comment('Tenants are now in maintenance mode.');
}
/** Get the payload to be placed in the "down" file. */
protected function getDownDatabasePayload()
{
return [
'except' => $this->excludedPaths(),
'redirect' => $this->redirectPath(),
'retry' => $this->getRetryTime(),
'refresh' => $this->option('refresh'),
'secret' => $this->option('secret'),
'status' => (int) $this->option('status', 503),
];
}
}

27
src/Commands/Up.php Normal file
View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Stancl\Tenancy\Commands;
use Illuminate\Console\Command;
use Stancl\Tenancy\Concerns\HasATenantsOption;
class Up extends Command
{
use HasATenantsOption;
protected $signature = 'tenants:up';
protected $description = 'Put tenants out of maintenance mode.';
public function handle(): void
{
tenancy()->runForMultiple($this->getTenants(), function ($tenant) {
$this->line("Tenant: {$tenant['id']}");
$tenant->bringUpFromMaintenance();
});
$this->comment('Tenants are now out of maintenance mode.');
}
}

View file

@ -15,6 +15,8 @@ trait DealsWithTenantSymlinks
* Tenants can have a symlink for each disk registered in the tenancy.filesystem.url_override config. * Tenants can have a symlink for each disk registered in the tenancy.filesystem.url_override config.
* *
* This is used for creating all possible tenant symlinks and removing all existing tenant symlinks. * This is used for creating all possible tenant symlinks and removing all existing tenant symlinks.
*
* @return Collection<string, string>
*/ */
protected static function possibleTenantSymlinks(Tenant $tenant): Collection protected static function possibleTenantSymlinks(Tenant $tenant): Collection
{ {
@ -33,7 +35,7 @@ trait DealsWithTenantSymlinks
}); });
} }
return $symlinks->mapWithKeys(fn ($item) => $item); return $symlinks->mapWithKeys(fn ($item) => $item); // [[a => b], [c => d]] -> [a => b, c => d]
} }
/** Determine if the provided path is an existing symlink. */ /** Determine if the provided path is an existing symlink. */

View file

@ -4,17 +4,27 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Database\Concerns; namespace Stancl\Tenancy\Database\Concerns;
use Carbon\Carbon; /**
* @mixin \Illuminate\Database\Eloquent\Model
*/
trait MaintenanceMode trait MaintenanceMode
{ {
public function putDownForMaintenance($data = []) public function putDownForMaintenance($data = []): void
{ {
$this->update(['maintenance_mode' => [ $this->update([
'time' => $data['time'] ?? Carbon::now()->getTimestamp(), 'maintenance_mode' => [
'message' => $data['message'] ?? null, 'except' => $data['except'] ?? null,
'retry' => $data['retry'] ?? null, 'redirect' => $data['redirect'] ?? null,
'allowed' => $data['allowed'] ?? [], 'retry' => $data['retry'] ?? null,
]]); 'refresh' => $data['refresh'] ?? null,
'secret' => $data['secret'] ?? null,
'status' => $data['status'] ?? 503,
],
]);
}
public function bringUpFromMaintenance(): void
{
$this->update(['maintenance_mode' => null]);
} }
} }

View file

@ -8,7 +8,7 @@ use Exception;
class DomainOccupiedByOtherTenantException extends Exception class DomainOccupiedByOtherTenantException extends Exception
{ {
public function __construct($domain) public function __construct(string $domain)
{ {
parent::__construct("The $domain domain is occupied by another tenant."); parent::__construct("The $domain domain is occupied by another tenant.");
} }

View file

@ -8,7 +8,7 @@ use Exception;
class TenancyNotInitializedException extends Exception class TenancyNotInitializedException extends Exception
{ {
public function __construct($message = '') public function __construct(string $message = '')
{ {
parent::__construct($message ?: 'Tenancy is not initialized.'); parent::__construct($message ?: 'Tenancy is not initialized.');
} }

View file

@ -24,7 +24,7 @@ class CreateDatabase implements ShouldQueue
) { ) {
} }
public function handle(DatabaseManager $databaseManager) public function handle(DatabaseManager $databaseManager): bool
{ {
event(new CreatingDatabase($this->tenant)); event(new CreatingDatabase($this->tenant));
@ -38,5 +38,7 @@ class CreateDatabase implements ShouldQueue
$this->tenant->database()->manager()->createDatabase($this->tenant); $this->tenant->database()->manager()->createDatabase($this->tenant);
event(new DatabaseCreated($this->tenant)); event(new DatabaseCreated($this->tenant));
return true;
} }
} }

View file

@ -16,24 +16,12 @@ class CreateStorageSymlinks implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public Tenant $tenant; public function __construct(
public Tenant $tenant,
/** ) {
* Create a new job instance.
*
* @return void
*/
public function __construct(Tenant $tenant)
{
$this->tenant = $tenant;
} }
/** public function handle(): void
* Execute the job.
*
* @return void
*/
public function handle()
{ {
CreateStorageSymlinksAction::handle($this->tenant); CreateStorageSymlinksAction::handle($this->tenant);
} }

View file

@ -9,14 +9,12 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
class DeleteDomains class DeleteDomains
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/** @var TenantWithDatabase&Model&HasDomains */ // todo unresolvable type for phpstan
protected TenantWithDatabase&Model $tenant; protected TenantWithDatabase&Model $tenant;
public function __construct(TenantWithDatabase&Model $tenant) public function __construct(TenantWithDatabase&Model $tenant)

View file

@ -7,7 +7,6 @@ namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode;
use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
@ -21,19 +20,38 @@ class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode
if (tenant('maintenance_mode')) { if (tenant('maintenance_mode')) {
$data = tenant('maintenance_mode'); $data = tenant('maintenance_mode');
if (isset($data['allowed']) && IpUtils::checkIp($request->ip(), (array) $data['allowed'])) { if (isset($data['secret']) && $request->path() === $data['secret']) {
return $this->bypassResponse($data['secret']);
}
if ($this->hasValidBypassCookie($request, $data) ||
$this->inExceptArray($request)) {
return $next($request); return $next($request);
} }
if ($this->inExceptArray($request)) { if (isset($data['redirect'])) {
return $next($request); $path = $data['redirect'] === '/'
? $data['redirect']
: trim($data['redirect'], '/');
if ($request->path() !== $path) {
return redirect($path);
}
}
if (isset($data['template'])) {
return response(
$data['template'],
(int) ($data['status'] ?? 503),
$this->getHeaders($data)
);
} }
throw new HttpException( throw new HttpException(
503, (int) ($data['status'] ?? 503),
'Service Unavailable', 'Service Unavailable',
null, null,
isset($data['retry']) ? ['Retry-After' => $data['retry']] : [] $this->getHeaders($data)
); );
} }

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Stancl\Tenancy\Middleware; namespace Stancl\Tenancy\Middleware;
use Closure; use Closure;
use Illuminate\Http\Request;
use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException; use Stancl\Tenancy\Contracts\TenantCouldNotBeIdentifiedException;
use Stancl\Tenancy\Contracts\TenantResolver; use Stancl\Tenancy\Contracts\TenantResolver;
use Stancl\Tenancy\Tenancy; use Stancl\Tenancy\Tenancy;
@ -17,7 +18,8 @@ abstract class IdentificationMiddleware
{ {
public static ?Closure $onFail = null; public static ?Closure $onFail = null;
public function initializeTenancy($request, $next, ...$resolverArguments) /** @return \Illuminate\Http\Response|mixed */
public function initializeTenancy(Request $request, Closure $next, mixed ...$resolverArguments): mixed
{ {
try { try {
$this->tenancy->initialize( $this->tenancy->initialize(

View file

@ -10,7 +10,7 @@ use Stancl\Tenancy\Exceptions\TenancyNotInitializedException;
class ScopeSessions class ScopeSessions
{ {
public static $tenantIdKey = '_tenant_id'; public static string $tenantIdKey = '_tenant_id';
/** @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

View file

@ -77,7 +77,9 @@ class TenancyServiceProvider extends ServiceProvider
public function boot(): void public function boot(): void
{ {
$this->commands([ $this->commands([
Commands\Up::class,
Commands\Run::class, Commands\Run::class,
Commands\Down::class,
Commands\Link::class, Commands\Link::class,
Commands\Seed::class, Commands\Seed::class,
Commands\Install::class, Commands\Install::class,
@ -86,8 +88,8 @@ class TenancyServiceProvider extends ServiceProvider
Commands\TenantList::class, Commands\TenantList::class,
Commands\TenantDump::class, Commands\TenantDump::class,
Commands\MigrateFresh::class, Commands\MigrateFresh::class,
Commands\CreatePendingTenants::class,
Commands\ClearPendingTenants::class, Commands\ClearPendingTenants::class,
Commands\CreatePendingTenants::class,
]); ]);
$this->publishes([ $this->publishes([

View file

@ -8,6 +8,7 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase;
use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase;
use Stancl\Tenancy\Database\Concerns\HasDomains; use Stancl\Tenancy\Database\Concerns\HasDomains;
use Stancl\Tenancy\Database\Concerns\HasPending; use Stancl\Tenancy\Database\Concerns\HasPending;
use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
use Stancl\Tenancy\Database\Models; use Stancl\Tenancy\Database\Models;
/** /**
@ -15,5 +16,5 @@ use Stancl\Tenancy\Database\Models;
*/ */
class Tenant extends Models\Tenant implements TenantWithDatabase class Tenant extends Models\Tenant implements TenantWithDatabase
{ {
use HasDatabase, HasDomains, HasPending; use HasDatabase, HasDomains, HasPending, MaintenanceMode;
} }

View file

@ -2,14 +2,14 @@
declare(strict_types=1); declare(strict_types=1);
use Illuminate\Support\Facades\Artisan;
use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Stancl\Tenancy\Database\Concerns\MaintenanceMode;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode;
use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain;
use Stancl\Tenancy\Tests\Etc\Tenant; use Stancl\Tenancy\Tests\Etc\Tenant;
test('tenant can be in maintenance mode', function () { test('tenants can be in maintenance mode', function () {
Route::get('/foo', function () { Route::get('/foo', function () {
return 'bar'; return 'bar';
})->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]); })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
@ -19,16 +19,40 @@ test('tenant can be in maintenance mode', function () {
'domain' => 'acme.localhost', 'domain' => 'acme.localhost',
]); ]);
pest()->get('http://acme.localhost/foo') pest()->get('http://acme.localhost/foo')->assertStatus(200);
->assertSuccessful();
tenancy()->end(); // flush stored tenant instance
$tenant->putDownForMaintenance(); $tenant->putDownForMaintenance();
pest()->expectException(HttpException::class); tenancy()->end(); // End tenancy before making a request
pest()->withoutExceptionHandling() pest()->get('http://acme.localhost/foo')->assertStatus(503);
->get('http://acme.localhost/foo');
$tenant->bringUpFromMaintenance();
tenancy()->end(); // End tenancy before making a request
pest()->get('http://acme.localhost/foo')->assertStatus(200);
});
test('tenants can be put into maintenance mode using artisan commands', function() {
Route::get('/foo', function () {
return 'bar';
})->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]);
$tenant = MaintenanceTenant::create();
$tenant->domains()->create([
'domain' => 'acme.localhost',
]);
pest()->get('http://acme.localhost/foo')->assertStatus(200);
Artisan::call('tenants:down');
tenancy()->end(); // End tenancy before making a request
pest()->get('http://acme.localhost/foo')->assertStatus(503);
Artisan::call('tenants:up');
tenancy()->end(); // End tenancy before making a request
pest()->get('http://acme.localhost/foo')->assertStatus(200);
}); });
class MaintenanceTenant extends Tenant class MaintenanceTenant extends Tenant