*/ use HasFactory; /** * Themes available for status pages. Each entry is [theme_id => display_name]. * These must also be enabled in resources/css/app.css @plugin block. * * @var array */ public const AVAILABLE_THEMES = [ 'business' => 'Business (Dark)', 'corporate' => 'Corporate (Light)', 'light' => 'Light', 'dark' => 'Dark', 'cupcake' => 'Cupcake', 'emerald' => 'Emerald', 'dracula' => 'Dracula', 'night' => 'Night', 'winter' => 'Winter', 'nord' => 'Nord', 'sunset' => 'Sunset', ]; protected $fillable = [ 'user_id', 'team_id', 'name', 'slug', 'description', 'footer_text', 'visibility', 'access_key', 'logo_path', 'theme', 'custom_css', 'custom_domain', 'domain_verified_at', ]; protected function casts(): array { return [ 'domain_verified_at' => 'datetime', ]; } /** * Generate a random access key for unlisted pages */ public static function generateAccessKey(): string { return Str::random(48); } /** * Generate a deterministic verification token for the current domain. * The user must add this as a TXT record at _easymonitor-verify.{domain}. */ public function domainVerificationToken(): string { if (! $this->custom_domain) { return ''; } return 'easymonitor-verify-'.substr( hash_hmac('sha256', $this->custom_domain.'|'.$this->id, config('app.key')), 0, 32 ); } /** * The hostname where the verification TXT record should be set. */ public function domainVerificationHost(): string { return '_easymonitor-verify.'.$this->custom_domain; } /** * Verify the custom domain by looking up the TXT record. * Marks domain_verified_at on success. */ public function verifyCustomDomain(): bool { if (! $this->custom_domain) { return false; } $expected = $this->domainVerificationToken(); $records = @dns_get_record($this->domainVerificationHost(), DNS_TXT); if (! is_array($records)) { return false; } foreach ($records as $record) { $value = $record['txt'] ?? ($record['entries'][0] ?? ''); if (is_string($value) && trim($value) === $expected) { $this->update(['domain_verified_at' => now()]); return true; } } return false; } public function isDomainVerified(): bool { return $this->custom_domain && $this->domain_verified_at !== null; } /** * Build the publicly-fetchable URL for the logo, regardless of which * storage disk holds it (local 'public' disk, R2, S3, etc.). * * Returns null if no logo is set. */ public function logoUrl(): ?string { if (! $this->logo_path) { return null; } $default = config('filesystems.default'); $disk = $default === 'local' ? 'public' : $default; try { return \Illuminate\Support\Facades\Storage::disk($disk)->url($this->logo_path); } catch (\Throwable) { return null; } } /** * The canonical public URL for this status page. * * Uses the verified custom domain when set, otherwise falls back to the * app-domain slug route. For unlisted pages the secret key is appended. */ public function publicUrl(): string { if ($this->isDomainVerified()) { $base = 'https://'.$this->custom_domain; } else { $base = route('public.status', $this->slug); } if ($this->visibility === 'unlisted' && $this->access_key) { return $base.'?key='.$this->access_key; } return $base; } /** * Resolve which theme name to apply on `data-theme`. * * If custom_css contains a DaisyUI `@plugin "daisyui/theme" { name: "X"; ... }` * block, "X" is used. Otherwise the configured theme is used. */ public function effectiveTheme(): string { if ($this->custom_css && preg_match('/@plugin\s+["\']daisyui\/theme["\']\s*\{[^}]*?name:\s*["\']([^"\']+)["\']\s*;/s', $this->custom_css, $m)) { return $m[1]; } return $this->theme ?: 'business'; } /** * Return CSS that's safe to inject inside a