diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 589838bc5..7e649ea56 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -10,6 +10,7 @@ 'operators' => [ '=>' => null, '|' => 'no_space', + '&' => 'no_space', ] ], 'blank_line_after_namespace' => true, diff --git a/composer.json b/composer.json index cc213add9..0dc9df09a 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,7 @@ "docker-m1": "ln -s docker-compose-m1.override.yml docker-compose.override.yml", "coverage": "open coverage/phpunit/html/index.html", "phpstan": "vendor/bin/phpstan", + "cs": "php-cs-fixer fix --config=.php-cs-fixer.php", "test": "PHP_VERSION=8.1 ./test --no-coverage", "test-full": "PHP_VERSION=8.1 ./test" }, diff --git a/src/Commands/Down.php b/src/Commands/Down.php new file mode 100644 index 000000000..96ed53356 --- /dev/null +++ b/src/Commands/Down.php @@ -0,0 +1,52 @@ +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), + ]; + } +} diff --git a/src/Commands/Up.php b/src/Commands/Up.php new file mode 100644 index 000000000..a3f690c2c --- /dev/null +++ b/src/Commands/Up.php @@ -0,0 +1,27 @@ +runForMultiple($this->getTenants(), function ($tenant) { + $this->line("Tenant: {$tenant['id']}"); + $tenant->bringUpFromMaintenance(); + }); + + $this->comment('Tenants are now out of maintenance mode.'); + } +} diff --git a/src/Database/Concerns/MaintenanceMode.php b/src/Database/Concerns/MaintenanceMode.php index 55e0e46de..cc4490f60 100644 --- a/src/Database/Concerns/MaintenanceMode.php +++ b/src/Database/Concerns/MaintenanceMode.php @@ -4,17 +4,27 @@ namespace Stancl\Tenancy\Database\Concerns; -use Carbon\Carbon; - +/** + * @mixin \Illuminate\Database\Eloquent\Model + */ trait MaintenanceMode { - public function putDownForMaintenance($data = []) + public function putDownForMaintenance($data = []): void + { + $this->update([ + 'maintenance_mode' => [ + 'except' => $data['except'] ?? null, + 'redirect' => $data['redirect'] ?? null, + 'retry' => $data['retry'] ?? null, + 'refresh' => $data['refresh'] ?? null, + 'secret' => $data['secret'] ?? null, + 'status' => $data['status'] ?? 503, + ], + ]); + } + + public function bringUpFromMaintenance(): void { - $this->update(['maintenance_mode' => [ - 'time' => $data['time'] ?? Carbon::now()->getTimestamp(), - 'message' => $data['message'] ?? null, - 'retry' => $data['retry'] ?? null, - 'allowed' => $data['allowed'] ?? [], - ]]); + $this->update(['maintenance_mode' => null]); } } diff --git a/src/Jobs/CreateStorageSymlinks.php b/src/Jobs/CreateStorageSymlinks.php index 2e1db88a2..fb9a3b0d4 100644 --- a/src/Jobs/CreateStorageSymlinks.php +++ b/src/Jobs/CreateStorageSymlinks.php @@ -18,7 +18,8 @@ class CreateStorageSymlinks implements ShouldQueue public function __construct( public Tenant $tenant, - ) {} + ) { + } public function handle(): void { diff --git a/src/Middleware/CheckTenantForMaintenanceMode.php b/src/Middleware/CheckTenantForMaintenanceMode.php index c1c734f53..58fcd184f 100644 --- a/src/Middleware/CheckTenantForMaintenanceMode.php +++ b/src/Middleware/CheckTenantForMaintenanceMode.php @@ -7,7 +7,6 @@ use Closure; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Stancl\Tenancy\Exceptions\TenancyNotInitializedException; -use Symfony\Component\HttpFoundation\IpUtils; use Symfony\Component\HttpKernel\Exception\HttpException; class CheckTenantForMaintenanceMode extends CheckForMaintenanceMode @@ -21,19 +20,38 @@ public function handle($request, Closure $next) if (tenant('maintenance_mode')) { $data = tenant('maintenance_mode'); - if (isset($data['allowed']) && IpUtils::checkIp($request->ip(), (array) $data['allowed'])) { - return $next($request); + if (isset($data['secret']) && $request->path() === $data['secret']) { + return $this->bypassResponse($data['secret']); } - if ($this->inExceptArray($request)) { + if ($this->hasValidBypassCookie($request, $data) || + $this->inExceptArray($request)) { return $next($request); } + if (isset($data['redirect'])) { + $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( - 503, + (int) ($data['status'] ?? 503), 'Service Unavailable', null, - isset($data['retry']) ? ['Retry-After' => $data['retry']] : [] + $this->getHeaders($data) ); } diff --git a/src/TenancyServiceProvider.php b/src/TenancyServiceProvider.php index b8eee487b..d95562834 100644 --- a/src/TenancyServiceProvider.php +++ b/src/TenancyServiceProvider.php @@ -86,6 +86,8 @@ public function boot(): void Commands\TenantList::class, Commands\TenantDump::class, Commands\MigrateFresh::class, + Commands\Down::class, + Commands\Up::class, ]); $this->publishes([ diff --git a/tests/Etc/Tenant.php b/tests/Etc/Tenant.php index f20b00006..9b59dedb4 100644 --- a/tests/Etc/Tenant.php +++ b/tests/Etc/Tenant.php @@ -7,6 +7,7 @@ use Stancl\Tenancy\Database\Contracts\TenantWithDatabase; use Stancl\Tenancy\Database\Concerns\HasDatabase; use Stancl\Tenancy\Database\Concerns\HasDomains; +use Stancl\Tenancy\Database\Concerns\MaintenanceMode; use Stancl\Tenancy\Database\Models; /** @@ -14,5 +15,5 @@ */ class Tenant extends Models\Tenant implements TenantWithDatabase { - use HasDatabase, HasDomains; + use HasDatabase, HasDomains, MaintenanceMode; } diff --git a/tests/MaintenanceModeTest.php b/tests/MaintenanceModeTest.php index 770dc5f25..6e28d1ab7 100644 --- a/tests/MaintenanceModeTest.php +++ b/tests/MaintenanceModeTest.php @@ -2,14 +2,14 @@ declare(strict_types=1); +use Illuminate\Support\Facades\Artisan; use Stancl\Tenancy\Database\Concerns\MaintenanceMode; -use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Support\Facades\Route; use Stancl\Tenancy\Middleware\CheckTenantForMaintenanceMode; use Stancl\Tenancy\Middleware\InitializeTenancyByDomain; 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 () { return 'bar'; })->middleware([InitializeTenancyByDomain::class, CheckTenantForMaintenanceMode::class]); @@ -19,16 +19,40 @@ 'domain' => 'acme.localhost', ]); - pest()->get('http://acme.localhost/foo') - ->assertSuccessful(); - - tenancy()->end(); // flush stored tenant instance + pest()->get('http://acme.localhost/foo')->assertStatus(200); $tenant->putDownForMaintenance(); - pest()->expectException(HttpException::class); - pest()->withoutExceptionHandling() - ->get('http://acme.localhost/foo'); + tenancy()->end(); // End tenancy before making a request + pest()->get('http://acme.localhost/foo')->assertStatus(503); + + $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