fix(admin): enforce hosts.datacenter FK (restrictOnDelete) — no deactivation side-effect
The DB now refuses to orphan a host; datacenter delete pre-checks for a friendly message and catches the constraint as the race backstop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
249efa0553
commit
8bcb5ec268
|
|
@ -4,7 +4,7 @@ namespace App\Livewire\Admin;
|
||||||
|
|
||||||
use App\Models\Datacenter;
|
use App\Models\Datacenter;
|
||||||
use App\Models\Host;
|
use App\Models\Host;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Database\QueryException;
|
||||||
use LivewireUI\Modal\ModalComponent;
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,24 +30,23 @@ class ConfirmDeleteDatacenter extends ModalComponent
|
||||||
|
|
||||||
public function delete()
|
public function delete()
|
||||||
{
|
{
|
||||||
// Serialize the guard with the delete: lock the datacenter row and
|
$dc = Datacenter::query()->where('uuid', $this->uuid)->first();
|
||||||
// re-check host membership inside the same transaction so a host created
|
|
||||||
// concurrently can't be orphaned (hosts.datacenter is a bare code).
|
|
||||||
DB::transaction(function () {
|
|
||||||
$dc = Datacenter::query()->where('uuid', $this->uuid)->lockForUpdate()->first();
|
|
||||||
if ($dc === null) {
|
if ($dc === null) {
|
||||||
return;
|
return $this->redirectRoute('admin.datacenters', navigate: true);
|
||||||
}
|
}
|
||||||
// Deactivate first so HostCreate's `exists:…,active,1` rule blocks any
|
|
||||||
// new host for this code while we finish deleting.
|
|
||||||
$dc->update(['active' => false]);
|
|
||||||
|
|
||||||
|
// The hosts.datacenter → datacenters.code foreign key (restrictOnDelete)
|
||||||
|
// is the source of truth: the DB refuses to orphan a host. We pre-check
|
||||||
|
// for a friendly message and catch the constraint as the race backstop.
|
||||||
if (Host::query()->where('datacenter', $dc->code)->exists()) {
|
if (Host::query()->where('datacenter', $dc->code)->exists()) {
|
||||||
return; // still has hosts — leave it (deactivated)
|
return $this->redirectRoute('admin.datacenters', navigate: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$dc->delete();
|
$dc->delete();
|
||||||
});
|
} catch (QueryException) {
|
||||||
|
// A host was created for this code concurrently — leave it intact.
|
||||||
|
}
|
||||||
|
|
||||||
return $this->redirectRoute('admin.datacenters', navigate: true);
|
return $this->redirectRoute('admin.datacenters', navigate: true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enforce hosts.datacenter → datacenters.code at the database level so a
|
||||||
|
* datacenter with hosts can never be deleted (restrictOnDelete), closing the
|
||||||
|
* check-then-delete race in the admin console. All existing host codes were
|
||||||
|
* backfilled into datacenters by 2026_07_25_090001.
|
||||||
|
*/
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Safety net: ensure every referenced code exists before adding the FK.
|
||||||
|
$now = now();
|
||||||
|
$missing = DB::table('hosts')
|
||||||
|
->whereNotNull('datacenter')
|
||||||
|
->whereNotIn('datacenter', fn ($q) => $q->from('datacenters')->select('code'))
|
||||||
|
->distinct()->pluck('datacenter');
|
||||||
|
foreach ($missing as $code) {
|
||||||
|
DB::table('datacenters')->insert([
|
||||||
|
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||||
|
'code' => $code, 'name' => \Illuminate\Support\Str::upper($code),
|
||||||
|
'active' => true, 'created_at' => $now, 'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::table('hosts', function (Blueprint $table) {
|
||||||
|
$table->foreign('datacenter')->references('code')->on('datacenters')->restrictOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('hosts', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['datacenter']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue