160 lines
5.5 KiB
PHP
160 lines
5.5 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Models\Concerns\HasUuid;
|
|
use Database\Factories\InstanceFactory;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
|
|
|
class Instance extends Model
|
|
{
|
|
/** @use HasFactory<InstanceFactory> */
|
|
use HasFactory, HasUuid;
|
|
|
|
protected $fillable = [
|
|
'customer_id', 'order_id', 'host_id', 'vmid', 'guest_ip', 'plan', 'quota_gb', 'traffic_addons', 'disk_gb',
|
|
'ram_mb', 'cores', 'subdomain', 'custom_domain', 'nc_admin_ref', 'admin_password', 'credentials_acknowledged_at',
|
|
'route_written', 'routed_hostnames', 'cert_ok', 'status', 'cancel_requested_at', 'service_ends_at',
|
|
'domain_token', 'domain_verified_at', 'domain_cert_ok', 'domain_checked_at', 'domain_error', 'domain_failures',
|
|
];
|
|
|
|
protected $hidden = ['nc_admin_ref', 'admin_password'];
|
|
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'nc_admin_ref' => 'encrypted',
|
|
// The initial Nextcloud admin password, held only until the
|
|
// customer acknowledges it on the dashboard (App\Livewire\
|
|
// Dashboard::acknowledgeCredentials). Same 'encrypted' cast as
|
|
// nc_admin_ref and Host::api_token_ref, for the same reason.
|
|
'admin_password' => 'encrypted',
|
|
'credentials_acknowledged_at' => 'datetime',
|
|
'domain_verified_at' => 'datetime',
|
|
'domain_checked_at' => 'datetime',
|
|
'domain_failures' => 'integer',
|
|
'domain_cert_ok' => 'boolean',
|
|
'route_written' => 'boolean',
|
|
'routed_hostnames' => 'array',
|
|
'cert_ok' => 'boolean',
|
|
'vmid' => 'integer',
|
|
'traffic_addons' => 'integer',
|
|
'quota_gb' => 'integer',
|
|
'disk_gb' => 'integer',
|
|
'ram_mb' => 'integer',
|
|
'cores' => 'integer',
|
|
'cancel_requested_at' => 'datetime',
|
|
'service_ends_at' => 'datetime',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Instances that are actually occupying storage on their host.
|
|
*
|
|
* A failed instance still holds its disk while its VM exists; a failure
|
|
* before any VM was created releases it. Kept here rather than written out
|
|
* at each call site so the host's own accounting and anything reporting on
|
|
* it cannot drift apart — a dashboard that counted differently from
|
|
* placement would call a host comfortable while orders were being refused
|
|
* on it.
|
|
*
|
|
* @param Builder<self> $query
|
|
*/
|
|
public function scopeOccupyingHost($query): void
|
|
{
|
|
$query->where(fn ($q) => $q->where('status', '!=', 'failed')->orWhereNotNull('vmid'));
|
|
}
|
|
|
|
/**
|
|
* Is the customer's own domain proven to be theirs?
|
|
*
|
|
* The one flag. A domain that has never been verified — or that lost its
|
|
* DNS proof and was withdrawn — is not this instance's address, whatever is
|
|
* typed in the column.
|
|
*/
|
|
public function domainIsVerified(): bool
|
|
{
|
|
return filled($this->custom_domain) && $this->domain_verified_at !== null;
|
|
}
|
|
|
|
/**
|
|
* Is the customer's own domain not merely proven, but actually answering?
|
|
*
|
|
* Verification says the domain is theirs. It says nothing about whether
|
|
* they have pointed it at us — that is an A record in their zone, which we
|
|
* cannot create and (behind a CDN) cannot even read. So there is a real
|
|
* state between "proven" and "working", it can last forever, and the portal
|
|
* has to be able to say which one the customer is in rather than printing
|
|
* an address that answers nothing.
|
|
*/
|
|
public function domainIsServed(): bool
|
|
{
|
|
return $this->domainIsVerified() && (bool) $this->domain_cert_ok;
|
|
}
|
|
|
|
/**
|
|
* The address this instance is actually served at.
|
|
*
|
|
* Every caller goes through here rather than reading `custom_domain ?:
|
|
* subdomain` for itself. That expression was the hole: it served, certified
|
|
* and trusted whatever hostname was in the column, so anybody who pointed
|
|
* any domain at the proxy got somebody else's Nextcloud under their own
|
|
* name — with a certificate to match.
|
|
*/
|
|
public function address(string $zone): string
|
|
{
|
|
return $this->domainIsVerified()
|
|
? (string) $this->custom_domain
|
|
: $this->subdomain.'.'.$zone;
|
|
}
|
|
|
|
public function customer(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Customer::class);
|
|
}
|
|
|
|
public function order(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Order::class);
|
|
}
|
|
|
|
/**
|
|
* The contract this machine fulfils. The authority on what the customer is
|
|
* entitled to — the catalogue only describes what we sell today.
|
|
*/
|
|
public function subscription(): HasOne
|
|
{
|
|
return $this->hasOne(Subscription::class);
|
|
}
|
|
|
|
public function host(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Host::class);
|
|
}
|
|
|
|
public function dnsRecords(): HasMany
|
|
{
|
|
return $this->hasMany(DnsRecord::class);
|
|
}
|
|
|
|
public function backups(): HasMany
|
|
{
|
|
return $this->hasMany(Backup::class);
|
|
}
|
|
|
|
public function monitoringTargets(): HasMany
|
|
{
|
|
return $this->hasMany(MonitoringTarget::class);
|
|
}
|
|
|
|
public function onboardingTasks(): HasMany
|
|
{
|
|
return $this->hasMany(OnboardingTask::class);
|
|
}
|
|
}
|