authorize('datacenters.manage'); // modals are reachable without the route middleware $dc = Datacenter::query()->where('uuid', $uuid)->firstOrFail(); $this->uuid = $uuid; $this->code = $dc->code; $this->name = $dc->name; $this->facility = (string) $dc->facility; $this->location = (string) $dc->location; $this->originalLocation = (string) $dc->location; $this->active = (bool) $dc->active; $this->hostCount = $this->hostsIn($dc->code); $this->orderCount = $this->ordersIn($dc->code); } /** * May this code still be changed? * * Always answered from the database, never from a hydrated property: the * lock is an authorisation decision, and a forged request that flipped it * would take the DNS names of every host in this datacenter with it. */ public function codeIsFree(string $code): bool { return $this->hostsIn($code) === 0 && $this->ordersIn($code) === 0; } private function hostsIn(string $code): int { return Host::query()->where('datacenter', $code)->count(); } private function ordersIn(string $code): int { return Order::query()->where('datacenter', $code)->count(); } public function save() { $this->authorize('datacenters.manage'); // Accept the configured countries plus the record's own legacy value, so a // datacenter created before the curated list can still be edited. The // legacy value is read from the DB — never from the client-hydrated // property, which could be forged to whitelist an arbitrary location. $dc = Datacenter::query()->where('uuid', $this->uuid)->firstOrFail(); $allowed = array_keys((array) config('countries')); if ($dc->location) { $allowed[] = $dc->location; } // Recomputed here, never taken from the property the browser sent back: // the lock is what protects every host's DNS name in this datacenter // from being orphaned, and a forged request must not be able to lift it. $codeIsFree = $this->codeIsFree($dc->code); if ($codeIsFree) { // Normalised before validating so the unique check matches how the // row is stored — otherwise `FSN` passes the rule and the update // then collides with an existing lowercased row (mirrors Datacenters). $this->code = strtolower(trim($this->code)); } else { // Whatever came back from the browser is discarded. Silently, and // deliberately: the field was never offered, so there is nothing to // report to an honest client, and a dishonest one gets no purchase. $this->code = $dc->code; } $rules = [ 'name' => 'required|string|max:255', 'facility' => 'nullable|string|max:255', 'location' => ['nullable', Rule::in($allowed)], // array form — a legacy value may contain commas 'active' => 'boolean', ]; if ($codeIsFree) { // The same rule the create form uses: a code becomes a DNS label // (fsn-01.node.clupilot.com), so it has to be one. Ignoring its own // row, or saving without touching the code would collide with itself. $rules['code'] = [ 'required', 'string', 'max:8', 'regex:/^[a-z0-9]([a-z0-9-]{0,6}[a-z0-9])?$/', Rule::unique('datacenters', 'code')->ignore($dc->id), ]; } $data = $this->validate($rules); // Only on the transition, and compared against what is STORED: an // already-inactive location would otherwise announce that it had just // been switched off every time its name was edited. $justDeactivated = $dc->active && ! $data['active']; $hostsLeftRunning = $justDeactivated ? $dc->hosts()->count() : 0; Datacenter::query()->where('uuid', $this->uuid)->update([ // Only where it was actually offered. $data has no 'code' key at // all when the field was locked, and writing $this->code back would // be the same value anyway — but only by luck, and this is the line // that would quietly stop being true if the lock ever moved. ...($codeIsFree ? ['code' => $data['code']] : []), 'name' => $data['name'], 'facility' => $data['facility'] ?: null, 'location' => $data['location'] ?: null, 'active' => $data['active'], ]); // One message, not two. Both go through the same toast, and the second // replaces the first — so the generic "saved" would swallow the only // sentence that says the hosts there are still running. $this->dispatch('notify', message: $hostsLeftRunning > 0 ? __('datacenters.deactivated_with_hosts', ['n' => $hostsLeftRunning]) : __('datacenters.updated')); return $this->redirectRoute('admin.datacenters', navigate: true); } public function render() { // Read again rather than shown from the mounted properties: a host can // be onboarded into this datacenter while the modal sits open, and an // input offered on the strength of a stale count would be refused on // save with nothing on screen explaining why. $stored = Datacenter::query()->where('uuid', $this->uuid)->value('code') ?? $this->code; $this->hostCount = $this->hostsIn($stored); $this->orderCount = $this->ordersIn($stored); return view('livewire.admin.edit-datacenter', [ 'countries' => config('countries'), 'codeFree' => $this->hostCount === 0 && $this->orderCount === 0, ]); } }