Deploy 2: 2FA (App + Email) + REST API + CSV import-export + auto backup
- Filament v5 multiFactorAuthentication enabled on both panels (App + Email) - HasAppAuthentication + HasEmailAuthentication on User and SuperAdmin - Migration: app_authentication_secret + recovery_codes + email_authentication_at - Sanctum REST API: /api/v1/login, /me, clients, vehicles, work-orders - EnsureTokenMatchesTenant middleware blocks cross-tenant token usage - CsvImportExport service: clients + vehicles bulk via plain CSV - Import/Export buttons on Client + Vehicle list pages - ApiTokens page in tenant panel (generate/revoke + last-used) - BackupAllTenantsCommand + scheduler (daily 03:00, retain 14 days) - Background scheduler in entrypoint.sh
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Central\Company;
|
||||
use App\Services\TenantBackupService;
|
||||
use App\Tenancy\TenantManager;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class BackupAllTenantsCommand extends Command
|
||||
{
|
||||
protected $signature = 'backup:tenants
|
||||
{--keep=14 : How many daily backups to retain per tenant}
|
||||
{--slug= : Backup only one tenant by slug}';
|
||||
|
||||
protected $description = 'Export ZIP backup for every active tenant. Stored in storage/app/backups/{date}/';
|
||||
|
||||
public function handle(TenantBackupService $service): int
|
||||
{
|
||||
$query = Company::query()->where('status', '!=', 'archived');
|
||||
if ($slug = $this->option('slug')) {
|
||||
$query->where('slug', $slug);
|
||||
}
|
||||
$companies = $query->get();
|
||||
|
||||
if ($companies->isEmpty()) {
|
||||
$this->warn('No tenants to back up.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$date = date('Y-m-d');
|
||||
$base = storage_path("app/backups/{$date}");
|
||||
if (! is_dir($base)) @mkdir($base, 0775, true);
|
||||
|
||||
$manager = app(TenantManager::class);
|
||||
$ok = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($companies as $company) {
|
||||
try {
|
||||
$manager->setCurrent($company);
|
||||
app(\Spatie\Permission\PermissionRegistrar::class)
|
||||
->setPermissionsTeamId($company->id);
|
||||
|
||||
$tmp = $service->export($company);
|
||||
$dest = "{$base}/{$company->slug}.zip";
|
||||
@rename($tmp, $dest);
|
||||
|
||||
$size = round(filesize($dest) / 1024, 1);
|
||||
$this->info("✓ {$company->slug} → {$size}KB");
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("✗ {$company->slug}: {$e->getMessage()}");
|
||||
$failed++;
|
||||
} finally {
|
||||
$manager->setCurrent(null);
|
||||
}
|
||||
}
|
||||
|
||||
$this->cleanupOld((int) $this->option('keep'));
|
||||
|
||||
$this->newLine();
|
||||
$this->info("Backup completed: {$ok} ok, {$failed} failed.");
|
||||
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
private function cleanupOld(int $keep): void
|
||||
{
|
||||
$backupsDir = storage_path('app/backups');
|
||||
if (! is_dir($backupsDir)) return;
|
||||
|
||||
$dates = collect(scandir($backupsDir))
|
||||
->filter(fn ($d) => preg_match('/^\d{4}-\d{2}-\d{2}$/', $d))
|
||||
->sortDesc()
|
||||
->values();
|
||||
|
||||
$toDelete = $dates->slice($keep);
|
||||
foreach ($toDelete as $dateDir) {
|
||||
$abs = "{$backupsDir}/{$dateDir}";
|
||||
if (is_dir($abs)) {
|
||||
array_map('unlink', glob("{$abs}/*"));
|
||||
@rmdir($abs);
|
||||
$this->line("Removed old backup: {$dateDir}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user