Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
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 EmailDomain extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_VERIFYING = 'verifying';
|
||||
public const STATUS_ACTIVE = 'active';
|
||||
public const STATUS_SUSPENDED = 'suspended';
|
||||
|
||||
public const METHOD_NAMESERVER = 'nameserver';
|
||||
public const METHOD_DNS_RECORD = 'dns_record';
|
||||
|
||||
public const FREE_MAILBOXES_LIMIT = 5;
|
||||
public const PAID_MAILBOX_PRICE_CEDIS = 5;
|
||||
public const SUBSCRIPTION_MONTHLY_PRICE_CEDIS = 5;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'email_server_id',
|
||||
'domain',
|
||||
'status',
|
||||
'verification_method',
|
||||
'verification_token',
|
||||
'ns_expected',
|
||||
'ns_observed',
|
||||
'ns_checked_at',
|
||||
'verified_at',
|
||||
'mail_ready_at',
|
||||
'free_mailboxes_limit',
|
||||
'paid_mailboxes_count',
|
||||
'dns_records',
|
||||
'dns_verification_status',
|
||||
'dns_last_checked_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ns_expected' => 'array',
|
||||
'ns_observed' => 'array',
|
||||
'ns_checked_at' => 'datetime',
|
||||
'verified_at' => 'datetime',
|
||||
'mail_ready_at' => 'datetime',
|
||||
'dns_records' => 'array',
|
||||
'dns_verification_status' => 'array',
|
||||
'dns_last_checked_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function emailServer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EmailServer::class);
|
||||
}
|
||||
|
||||
public function mailboxes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Mailbox::class, 'email_domain_id');
|
||||
}
|
||||
|
||||
public function dkimKeys(): HasMany
|
||||
{
|
||||
return $this->hasMany(EmailDomainDkimKey::class);
|
||||
}
|
||||
|
||||
public function activeDkimKey(): HasOne
|
||||
{
|
||||
return $this->hasOne(EmailDomainDkimKey::class)->where('status', 'deployed')->latest();
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
public function isVerifying(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_VERIFYING;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_ACTIVE;
|
||||
}
|
||||
|
||||
public function isSuspended(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_SUSPENDED;
|
||||
}
|
||||
|
||||
public function isReadyForMail(): bool
|
||||
{
|
||||
// Domain is ready for mail if it's active (verified)
|
||||
// mail_ready_at is set when infrastructure is provisioned, but we allow
|
||||
// mailbox creation as soon as domain is verified
|
||||
return $this->isActive();
|
||||
}
|
||||
|
||||
public function usesNameserverVerification(): bool
|
||||
{
|
||||
return $this->verification_method === self::METHOD_NAMESERVER;
|
||||
}
|
||||
|
||||
public function usesDnsRecordVerification(): bool
|
||||
{
|
||||
return $this->verification_method === self::METHOD_DNS_RECORD;
|
||||
}
|
||||
|
||||
public function getMailboxCountAttribute(): int
|
||||
{
|
||||
return $this->mailboxes()->count();
|
||||
}
|
||||
|
||||
public function getFreeMailboxesLimitAttribute($value): int
|
||||
{
|
||||
$base = $value ?? self::FREE_MAILBOXES_LIMIT;
|
||||
|
||||
// Check if user has hosting accounts with higher email quotas
|
||||
$hostingAllowance = $this->getHostingEmailAllowance();
|
||||
|
||||
return max($base, $hostingAllowance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the email allowance from the user's hosting accounts for this domain.
|
||||
*/
|
||||
public function getHostingEmailAllowance(): int
|
||||
{
|
||||
$hostingAccount = HostingAccount::query()
|
||||
->where('user_id', $this->user_id)
|
||||
->where('status', HostingAccount::STATUS_ACTIVE)
|
||||
->whereHas('sites', fn ($q) => $q->where('domain', $this->domain))
|
||||
->with('product')
|
||||
->first();
|
||||
|
||||
if (! $hostingAccount) {
|
||||
// Also check primary_domain
|
||||
$hostingAccount = HostingAccount::query()
|
||||
->where('user_id', $this->user_id)
|
||||
->where('status', HostingAccount::STATUS_ACTIVE)
|
||||
->where('primary_domain', $this->domain)
|
||||
->with('product')
|
||||
->first();
|
||||
}
|
||||
|
||||
if (! $hostingAccount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $hostingAccount->freeEmailAllowance() ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get per-record DNS verification results.
|
||||
*/
|
||||
public function getDnsRecordStatuses(): array
|
||||
{
|
||||
return $this->dns_verification_status ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an authentication record (spf|dkim|dmarc) should display as
|
||||
* verified. The per-record dns_verification_status map is only populated by
|
||||
* the DNS-record verification path; nameserver-managed / Ladill-owned domains
|
||||
* (we publish their DNS) leave it empty even when fully configured. So an
|
||||
* active domain necessarily has SPF + DKIM in place (required to activate),
|
||||
* and DMARC is published for nameserver-managed domains.
|
||||
*/
|
||||
public function authVerified(string $key): bool
|
||||
{
|
||||
if (($this->getDnsRecordStatuses()[$key]['status'] ?? null) === 'verified') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return match ($key) {
|
||||
'spf', 'dkim' => $this->isActive(),
|
||||
'dmarc' => $this->isActive() && $this->usesNameserverVerification(),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all required DNS records are verified.
|
||||
*/
|
||||
public function allDnsRecordsVerified(): bool
|
||||
{
|
||||
$statuses = $this->getDnsRecordStatuses();
|
||||
|
||||
if (empty($statuses)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All required records must be verified
|
||||
foreach ($statuses as $record) {
|
||||
if (($record['required'] ?? true) && ($record['status'] ?? 'pending') !== 'verified') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count verified and total required DNS records.
|
||||
*/
|
||||
public function dnsVerificationProgress(): array
|
||||
{
|
||||
$statuses = $this->getDnsRecordStatuses();
|
||||
$required = collect($statuses)->filter(fn ($r) => $r['required'] ?? true);
|
||||
|
||||
return [
|
||||
'total' => $required->count(),
|
||||
'verified' => $required->where('status', 'verified')->count(),
|
||||
'pending' => $required->where('status', '!=', 'verified')->count(),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFreeMailboxesUsedAttribute(): int
|
||||
{
|
||||
return $this->mailboxes()->where('is_paid', false)->count();
|
||||
}
|
||||
|
||||
public function getPaidMailboxesUsedAttribute(): int
|
||||
{
|
||||
return $this->mailboxes()->where('is_paid', true)->count();
|
||||
}
|
||||
|
||||
public function getFreeMailboxesRemainingAttribute(): int
|
||||
{
|
||||
return max(0, $this->free_mailboxes_limit - $this->free_mailboxes_used);
|
||||
}
|
||||
|
||||
public function canCreateFreeMailbox(): bool
|
||||
{
|
||||
return $this->free_mailboxes_remaining > 0;
|
||||
}
|
||||
|
||||
public function requiresPaymentForNextMailbox(): bool
|
||||
{
|
||||
return !$this->canCreateFreeMailbox();
|
||||
}
|
||||
|
||||
public function monthlySubscriptionPrice(): float
|
||||
{
|
||||
return self::SUBSCRIPTION_MONTHLY_PRICE_CEDIS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user