some progress
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckDomainExpiry;
|
||||
use App\Models\Domain;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MonitorDomains extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:domains';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check expiry dates for all active domains';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting domain expiry monitoring...');
|
||||
|
||||
$domains = Domain::where('is_active', true)->get();
|
||||
|
||||
if ($domains->isEmpty()) {
|
||||
$this->warn('No active domains found to monitor.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$domains->count()} active domain(s) to check.");
|
||||
|
||||
$progressBar = $this->output->createProgressBar($domains->count());
|
||||
$progressBar->start();
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
CheckDomainExpiry::dispatch($domain);
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine();
|
||||
|
||||
$this->info('Domain expiry check jobs have been dispatched.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckSitemap;
|
||||
use App\Models\Sitemap;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class MonitorSitemaps extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:sitemaps';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check all active sitemaps for validity and accessibility';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting sitemap monitoring...');
|
||||
|
||||
$sitemaps = Sitemap::where('is_active', true)->get();
|
||||
|
||||
if ($sitemaps->isEmpty()) {
|
||||
$this->warn('No active sitemaps found to monitor.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$sitemaps->count()} active sitemap(s) to check.");
|
||||
|
||||
$progressBar = $this->output->createProgressBar($sitemaps->count());
|
||||
$progressBar->start();
|
||||
|
||||
foreach ($sitemaps as $sitemap) {
|
||||
CheckSitemap::dispatch($sitemap);
|
||||
$progressBar->advance();
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$this->newLine();
|
||||
|
||||
$this->info('Sitemap check jobs have been dispatched.');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Models\BrokenLink;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BrokenLinkController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display broken links overview for all sites
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$sites = Site::where('user_id', auth()->id())
|
||||
->withCount('brokenLinks')
|
||||
->get();
|
||||
|
||||
// Get broken link statistics
|
||||
$stats = [
|
||||
'total_sites' => $sites->count(),
|
||||
'sites_with_broken_links' => $sites->where('broken_links_count', '>', 0)->count(),
|
||||
'total_broken_links' => $sites->sum('broken_links_count'),
|
||||
'sites_clean' => $sites->where('broken_links_count', 0)->count(),
|
||||
];
|
||||
|
||||
// Get recent broken links across all sites
|
||||
$recentBrokenLinks = BrokenLink::whereHas('site', function ($query) {
|
||||
$query->where('user_id', auth()->id());
|
||||
})
|
||||
->with('site')
|
||||
->latest()
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return view('broken-links.index', compact('sites', 'stats', 'recentBrokenLinks'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display broken links for a specific site
|
||||
*/
|
||||
public function show(Site $site)
|
||||
{
|
||||
$this->authorize('view', $site);
|
||||
|
||||
$brokenLinks = $site->brokenLinks()
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(50);
|
||||
|
||||
$stats = [
|
||||
'total_broken_links' => $brokenLinks->total(),
|
||||
'unique_urls' => $site->brokenLinks()->distinct('url')->count('url'),
|
||||
];
|
||||
|
||||
return view('broken-links.show', compact('site', 'brokenLinks', 'stats'));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,27 @@ use Illuminate\View\View;
|
||||
|
||||
class DnsRecordController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display overview of all DNS records across all sites
|
||||
*/
|
||||
public function overview(): View
|
||||
{
|
||||
$sites = Site::where('user_id', Auth::id())
|
||||
->withCount('dnsRecords')
|
||||
->with(['dnsRecords' => function ($query) {
|
||||
$query->where('is_active', true)->limit(5);
|
||||
}])
|
||||
->get();
|
||||
|
||||
$stats = [
|
||||
'total_sites' => $sites->count(),
|
||||
'total_dns_records' => $sites->sum('dns_records_count'),
|
||||
'sites_with_records' => $sites->where('dns_records_count', '>', 0)->count(),
|
||||
];
|
||||
|
||||
return view('dns-records.overview', compact('sites', 'stats'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the DNS records for a site.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\CheckDomainExpiry;
|
||||
use App\Models\Domain;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DomainController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$domains = Domain::with('site')
|
||||
->where('is_active', true)
|
||||
->orderBy('expires_at', 'asc')
|
||||
->paginate(15);
|
||||
|
||||
return view('domains.index', compact('domains'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$sites = Site::where('user_id', auth()->id())
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('domains.create', compact('sites'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'site_id' => 'required|exists:sites,id',
|
||||
'domain' => 'required|string|max:255',
|
||||
'is_active' => 'sometimes|nullable|in:on,off',
|
||||
]);
|
||||
|
||||
$validated['is_active'] = $request->has('is_active');
|
||||
|
||||
$domain = Domain::create($validated);
|
||||
|
||||
// Dispatch job to check domain immediately
|
||||
CheckDomainExpiry::dispatch($domain);
|
||||
|
||||
return redirect()
|
||||
->route('domains.index')
|
||||
->with('success', 'Domain added successfully. Checking expiry date...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Domain $domain)
|
||||
{
|
||||
$domain->load('site');
|
||||
|
||||
return view('domains.show', compact('domain'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Domain $domain)
|
||||
{
|
||||
$sites = Site::where('user_id', auth()->id())
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('domains.edit', compact('domain', 'sites'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Domain $domain)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'site_id' => 'required|exists:sites,id',
|
||||
'domain' => 'required|string|max:255',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$validated['is_active'] = $request->has('is_active');
|
||||
|
||||
$domain->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('domains.index')
|
||||
->with('success', 'Domain updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Domain $domain)
|
||||
{
|
||||
$domain->delete();
|
||||
|
||||
return redirect()
|
||||
->route('domains.index')
|
||||
->with('success', 'Domain deleted successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger domain expiry check
|
||||
*/
|
||||
public function check(Domain $domain)
|
||||
{
|
||||
CheckDomainExpiry::dispatch($domain);
|
||||
|
||||
return back()
|
||||
->with('success', 'Domain expiry check has been queued.');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,27 @@ use Illuminate\View\View;
|
||||
|
||||
class HealthCheckController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display overview of all health checks across all sites
|
||||
*/
|
||||
public function overview(): View
|
||||
{
|
||||
$sites = Site::where('user_id', Auth::id())
|
||||
->withCount('healthChecks')
|
||||
->with(['healthChecks' => function ($query) {
|
||||
$query->where('is_active', true)->limit(5);
|
||||
}])
|
||||
->get();
|
||||
|
||||
$stats = [
|
||||
'total_sites' => $sites->count(),
|
||||
'total_health_checks' => $sites->sum('health_checks_count'),
|
||||
'sites_with_checks' => $sites->where('health_checks_count', '>', 0)->count(),
|
||||
];
|
||||
|
||||
return view('health-checks.overview', compact('sites', 'stats'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the health checks for a site.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\CheckSitemap;
|
||||
use App\Models\Sitemap;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SitemapController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$sitemaps = Sitemap::with('site')
|
||||
->where('is_active', true)
|
||||
->orderBy('is_valid', 'asc')
|
||||
->orderBy('last_checked_at', 'desc')
|
||||
->paginate(15);
|
||||
|
||||
return view('sitemaps.index', compact('sitemaps'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$sites = Site::where('user_id', auth()->id())
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('sitemaps.create', compact('sites'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'site_id' => 'required|exists:sites,id',
|
||||
'url' => 'required|url|max:500',
|
||||
'is_active' => 'sometimes|nullable|in:on,off',
|
||||
]);
|
||||
|
||||
$validated['is_active'] = $request->has('is_active');
|
||||
|
||||
$sitemap = Sitemap::create($validated);
|
||||
|
||||
// Dispatch job to check sitemap immediately
|
||||
CheckSitemap::dispatch($sitemap);
|
||||
|
||||
return redirect()
|
||||
->route('sitemaps.index')
|
||||
->with('success', 'Sitemap added successfully. Validating now...');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Sitemap $sitemap)
|
||||
{
|
||||
$sitemap->load('site');
|
||||
|
||||
return view('sitemaps.show', compact('sitemap'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Sitemap $sitemap)
|
||||
{
|
||||
$sites = Site::where('user_id', auth()->id())
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return view('sitemaps.edit', compact('sitemap', 'sites'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Sitemap $sitemap)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'site_id' => 'required|exists:sites,id',
|
||||
'url' => 'required|url|max:500',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
$validated['is_active'] = $request->has('is_active');
|
||||
|
||||
$sitemap->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('sitemaps.index')
|
||||
->with('success', 'Sitemap updated successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Sitemap $sitemap)
|
||||
{
|
||||
$sitemap->delete();
|
||||
|
||||
return redirect()
|
||||
->route('sitemaps.index')
|
||||
->with('success', 'Sitemap deleted successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger sitemap validation
|
||||
*/
|
||||
public function check(Sitemap $sitemap)
|
||||
{
|
||||
CheckSitemap::dispatch($sitemap);
|
||||
|
||||
return back()
|
||||
->with('success', 'Sitemap validation has been queued.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Models\SslCertificate;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SslCertificateController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display SSL certificate overview for all sites
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$sites = Site::where('user_id', auth()->id())
|
||||
->with(['sslCertificates' => function ($query) {
|
||||
$query->latest()->limit(1);
|
||||
}])
|
||||
->get();
|
||||
|
||||
// Get SSL statistics
|
||||
$stats = [
|
||||
'total_sites' => $sites->count(),
|
||||
'valid_certs' => 0,
|
||||
'expiring_soon' => 0,
|
||||
'expired' => 0,
|
||||
];
|
||||
|
||||
foreach ($sites as $site) {
|
||||
$latestCert = $site->sslCertificates->first();
|
||||
if ($latestCert) {
|
||||
if ($latestCert->expires_at) {
|
||||
$daysUntilExpiry = now()->diffInDays($latestCert->expires_at, false);
|
||||
|
||||
if ($daysUntilExpiry < 0) {
|
||||
$stats['expired']++;
|
||||
} elseif ($daysUntilExpiry <= 30) {
|
||||
$stats['expiring_soon']++;
|
||||
} else {
|
||||
$stats['valid_certs']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get recent SSL checks across all sites
|
||||
$recentCertificates = SslCertificate::whereHas('site', function ($query) {
|
||||
$query->where('user_id', auth()->id());
|
||||
})
|
||||
->with('site')
|
||||
->latest()
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return view('ssl.index', compact('sites', 'stats', 'recentCertificates'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display SSL certificate details for a specific site
|
||||
*/
|
||||
public function show(Site $site)
|
||||
{
|
||||
$this->authorize('view', $site);
|
||||
|
||||
$sslCertificates = $site->sslCertificates()
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(50);
|
||||
|
||||
$latestCert = $site->sslCertificates()->latest()->first();
|
||||
|
||||
return view('ssl.show', compact('site', 'sslCertificates', 'latestCert'));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,27 @@ use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TcpPortController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display overview of all TCP ports across all sites
|
||||
*/
|
||||
public function overview()
|
||||
{
|
||||
$sites = Site::where('user_id', Auth::id())
|
||||
->withCount('tcpPorts')
|
||||
->with(['tcpPorts' => function ($query) {
|
||||
$query->where('is_active', true)->limit(5);
|
||||
}])
|
||||
->get();
|
||||
|
||||
$stats = [
|
||||
'total_sites' => $sites->count(),
|
||||
'total_ports_monitored' => $sites->sum('tcp_ports_count'),
|
||||
'sites_with_ports' => $sites->where('tcp_ports_count', '>', 0)->count(),
|
||||
];
|
||||
|
||||
return view('tcp-ports.overview', compact('sites', 'stats'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Models\UptimeCheck;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UptimeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display uptime monitoring overview for all sites
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$sites = Site::where('user_id', auth()->id())
|
||||
->with(['uptimeChecks' => function ($query) {
|
||||
$query->latest()->limit(1);
|
||||
}])
|
||||
->get();
|
||||
|
||||
// Get uptime statistics
|
||||
$stats = [
|
||||
'total_sites' => $sites->count(),
|
||||
'sites_up' => 0,
|
||||
'sites_down' => 0,
|
||||
'avg_response_time' => 0,
|
||||
];
|
||||
|
||||
$totalResponseTime = 0;
|
||||
$checksCount = 0;
|
||||
|
||||
foreach ($sites as $site) {
|
||||
$latestCheck = $site->uptimeChecks->first();
|
||||
if ($latestCheck) {
|
||||
if ($latestCheck->is_online) {
|
||||
$stats['sites_up']++;
|
||||
} else {
|
||||
$stats['sites_down']++;
|
||||
}
|
||||
$totalResponseTime += $latestCheck->response_time ?? 0;
|
||||
$checksCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($checksCount > 0) {
|
||||
$stats['avg_response_time'] = round($totalResponseTime / $checksCount, 2);
|
||||
}
|
||||
|
||||
// Get recent checks across all sites
|
||||
$recentChecks = UptimeCheck::whereHas('site', function ($query) {
|
||||
$query->where('user_id', auth()->id());
|
||||
})
|
||||
->with('site')
|
||||
->latest()
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return view('uptime.index', compact('sites', 'stats', 'recentChecks'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display uptime details for a specific site
|
||||
*/
|
||||
public function show(Site $site)
|
||||
{
|
||||
$this->authorize('view', $site);
|
||||
|
||||
$uptimeChecks = $site->uptimeChecks()
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(100);
|
||||
|
||||
// Calculate uptime percentage for last 30 days
|
||||
$checksLast30Days = $site->uptimeChecks()
|
||||
->where('created_at', '>=', now()->subDays(30))
|
||||
->get();
|
||||
|
||||
$uptimePercentage = 0;
|
||||
if ($checksLast30Days->count() > 0) {
|
||||
$upCount = $checksLast30Days->where('is_online', true)->count();
|
||||
$uptimePercentage = round(($upCount / $checksLast30Days->count()) * 100, 2);
|
||||
}
|
||||
|
||||
return view('uptime.show', compact('site', 'uptimeChecks', 'uptimePercentage'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Domain;
|
||||
use App\Models\Notification;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckDomainExpiry implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Domain $domain
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// Use WHOIS API to check domain expiry
|
||||
// We'll use the WHOIS XML API (https://www.whoisxmlapi.com/)
|
||||
// You can also use other providers like whoisfreaks.com or domainr.com
|
||||
|
||||
$apiKey = config('services.whois.api_key');
|
||||
|
||||
if (!$apiKey) {
|
||||
Log::warning('WHOIS API key not configured');
|
||||
return;
|
||||
}
|
||||
|
||||
$response = Http::timeout(30)
|
||||
->get('https://www.whoisxmlapi.com/whoisserver/WhoisService', [
|
||||
'apiKey' => $apiKey,
|
||||
'domainName' => $this->domain->domain,
|
||||
'outputFormat' => 'JSON',
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
|
||||
// Parse expiry date from WHOIS response
|
||||
$expiryDate = $this->parseExpiryDate($data);
|
||||
|
||||
if ($expiryDate) {
|
||||
$this->domain->update([
|
||||
'expires_at' => $expiryDate,
|
||||
'last_checked_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
// Check if domain is expiring soon (within 30 days)
|
||||
if ($this->domain->isExpiringSoon()) {
|
||||
$this->sendExpiryAlert();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log::error('WHOIS API request failed', [
|
||||
'domain' => $this->domain->domain,
|
||||
'status' => $response->status(),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->domain->update([
|
||||
'last_checked_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error checking domain expiry', [
|
||||
'domain' => $this->domain->domain,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e; // Re-throw to allow job retry
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse expiry date from WHOIS response
|
||||
*/
|
||||
protected function parseExpiryDate(array $data): ?Carbon
|
||||
{
|
||||
// Try multiple possible fields where expiry date might be
|
||||
$possibleFields = [
|
||||
'WhoisRecord.registryData.expiresDate',
|
||||
'WhoisRecord.expiresDate',
|
||||
'expiresDate',
|
||||
];
|
||||
|
||||
foreach ($possibleFields as $field) {
|
||||
$value = data_get($data, $field);
|
||||
|
||||
if ($value) {
|
||||
try {
|
||||
return Carbon::parse($value);
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Failed to parse date', [
|
||||
'field' => $field,
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send alert notification for expiring domain
|
||||
*/
|
||||
protected function sendExpiryAlert(): void
|
||||
{
|
||||
$daysUntilExpiry = $this->domain->daysUntilExpiration();
|
||||
|
||||
$message = $this->domain->isExpired()
|
||||
? "Domain '{$this->domain->domain}' has expired!"
|
||||
: "Domain '{$this->domain->domain}' expires in {$daysUntilExpiry} days (on {$this->domain->expires_at->format('Y-m-d')})";
|
||||
|
||||
Notification::create([
|
||||
'user_id' => $this->domain->site->user_id,
|
||||
'type' => 'domain_expiry',
|
||||
'message' => $message,
|
||||
'data' => json_encode([
|
||||
'domain_id' => $this->domain->id,
|
||||
'site_id' => $this->domain->site_id,
|
||||
'domain' => $this->domain->domain,
|
||||
'expires_at' => $this->domain->expires_at,
|
||||
'days_until_expiry' => $daysUntilExpiry,
|
||||
]),
|
||||
'read_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Notification;
|
||||
use App\Models\Sitemap;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckSitemap implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Sitemap $sitemap
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// Fetch the sitemap URL
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders([
|
||||
'User-Agent' => 'Mozilla/5.0 (compatible; SitemapMonitor/1.0)',
|
||||
])
|
||||
->get($this->sitemap->url);
|
||||
|
||||
if (!$response->successful()) {
|
||||
$this->sitemap->markAsInvalid(
|
||||
"HTTP {$response->status()}: Failed to fetch sitemap"
|
||||
);
|
||||
$this->sendAlert("Sitemap is inaccessible (HTTP {$response->status()})");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the response body
|
||||
$xmlContent = $response->body();
|
||||
|
||||
// Validate XML structure using simplexml_load_string
|
||||
libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($xmlContent);
|
||||
|
||||
if ($xml === false) {
|
||||
$errors = libxml_get_errors();
|
||||
$errorMessages = array_map(fn($error) => trim($error->message), $errors);
|
||||
libxml_clear_errors();
|
||||
|
||||
$this->sitemap->markAsInvalid(
|
||||
'Invalid XML: ' . implode('; ', $errorMessages)
|
||||
);
|
||||
$this->sendAlert('Sitemap contains invalid XML');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a valid sitemap (has urlset or sitemapindex)
|
||||
$namespaces = $xml->getNamespaces(true);
|
||||
$isUrlset = isset($xml->url) || isset($namespaces['']);
|
||||
$isSitemapIndex = isset($xml->sitemap);
|
||||
|
||||
if (!$isUrlset && !$isSitemapIndex) {
|
||||
$this->sitemap->markAsInvalid(
|
||||
'Invalid sitemap format: Missing <urlset> or <sitemapindex> root element'
|
||||
);
|
||||
$this->sendAlert('Sitemap has invalid format');
|
||||
return;
|
||||
}
|
||||
|
||||
// Count URLs in the sitemap
|
||||
$urlCount = $this->countUrls($xml);
|
||||
|
||||
// Mark as valid
|
||||
$this->sitemap->markAsValid($urlCount);
|
||||
|
||||
Log::info('Sitemap validated successfully', [
|
||||
'sitemap_id' => $this->sitemap->id,
|
||||
'url' => $this->sitemap->url,
|
||||
'url_count' => $urlCount,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error checking sitemap', [
|
||||
'sitemap_id' => $this->sitemap->id,
|
||||
'url' => $this->sitemap->url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$this->sitemap->markAsInvalid(
|
||||
'Exception: ' . $e->getMessage()
|
||||
);
|
||||
$this->sendAlert('Sitemap check failed: ' . $e->getMessage());
|
||||
|
||||
throw $e; // Re-throw to allow job retry
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count URLs in the sitemap
|
||||
*/
|
||||
protected function countUrls(\SimpleXMLElement $xml): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
// Register namespaces
|
||||
$xml->registerXPathNamespace('sm', 'http://www.sitemaps.org/schemas/sitemap/0.9');
|
||||
|
||||
// Check for regular sitemap
|
||||
$urls = $xml->xpath('//sm:url') ?: $xml->xpath('//url');
|
||||
if ($urls !== false) {
|
||||
$count = count($urls);
|
||||
}
|
||||
|
||||
// If it's a sitemap index, count the sitemaps instead
|
||||
if ($count === 0) {
|
||||
$sitemaps = $xml->xpath('//sm:sitemap') ?: $xml->xpath('//sitemap');
|
||||
if ($sitemaps !== false) {
|
||||
$count = count($sitemaps);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send alert notification for invalid sitemap
|
||||
*/
|
||||
protected function sendAlert(string $message): void
|
||||
{
|
||||
Notification::create([
|
||||
'user_id' => $this->sitemap->site->user_id,
|
||||
'type' => 'sitemap_invalid',
|
||||
'message' => "{$message}: {$this->sitemap->url}",
|
||||
'data' => json_encode([
|
||||
'sitemap_id' => $this->sitemap->id,
|
||||
'site_id' => $this->sitemap->site_id,
|
||||
'url' => $this->sitemap->url,
|
||||
'error' => $this->sitemap->last_error,
|
||||
]),
|
||||
'read_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class Domain extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'domain',
|
||||
'expires_at',
|
||||
'last_checked_at',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'last_checked_at' => 'datetime',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if domain is expiring soon (within 30 days)
|
||||
*/
|
||||
public function isExpiringSoon(): bool
|
||||
{
|
||||
if (!$this->expires_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->expires_at->lte(Carbon::now()->addDays(30));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if domain has expired
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
if (!$this->expires_at) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->expires_at->lte(Carbon::now());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get days until expiration
|
||||
*/
|
||||
public function daysUntilExpiration(): ?int
|
||||
{
|
||||
if (!$this->expires_at) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Carbon::now()->diffInDays($this->expires_at, false);
|
||||
}
|
||||
}
|
||||
@@ -53,4 +53,14 @@ class Site extends Model
|
||||
{
|
||||
return $this->hasMany(DnsRecord::class);
|
||||
}
|
||||
|
||||
public function domains(): HasMany
|
||||
{
|
||||
return $this->hasMany(Domain::class);
|
||||
}
|
||||
|
||||
public function sitemaps(): HasMany
|
||||
{
|
||||
return $this->hasMany(Sitemap::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Sitemap extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'url',
|
||||
'last_checked_at',
|
||||
'is_valid',
|
||||
'last_error',
|
||||
'is_active',
|
||||
'url_count',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_checked_at' => 'datetime',
|
||||
'is_valid' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'url_count' => 'integer',
|
||||
];
|
||||
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sitemap has errors
|
||||
*/
|
||||
public function hasErrors(): bool
|
||||
{
|
||||
return !$this->is_valid || !empty($this->last_error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status badge color
|
||||
*/
|
||||
public function getStatusBadgeClass(): string
|
||||
{
|
||||
if (!$this->last_checked_at) {
|
||||
return 'bg-secondary';
|
||||
}
|
||||
|
||||
return $this->is_valid ? 'bg-success' : 'bg-danger';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status text
|
||||
*/
|
||||
public function getStatusText(): string
|
||||
{
|
||||
if (!$this->last_checked_at) {
|
||||
return 'Not Checked';
|
||||
}
|
||||
|
||||
return $this->is_valid ? 'Valid' : 'Invalid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark sitemap as valid
|
||||
*/
|
||||
public function markAsValid(?int $urlCount = null): void
|
||||
{
|
||||
$this->update([
|
||||
'is_valid' => true,
|
||||
'last_error' => null,
|
||||
'last_checked_at' => now(),
|
||||
'url_count' => $urlCount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark sitemap as invalid
|
||||
*/
|
||||
public function markAsInvalid(string $error): void
|
||||
{
|
||||
$this->update([
|
||||
'is_valid' => false,
|
||||
'last_error' => $error,
|
||||
'last_checked_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,16 @@ class User extends Authenticatable
|
||||
'email',
|
||||
'password',
|
||||
'notification_preferences',
|
||||
'notification_channels',
|
||||
'slack_webhook_url',
|
||||
'telegram_chat_id',
|
||||
'discord_webhook_url',
|
||||
'teams_webhook_url',
|
||||
'google_chat_webhook_url',
|
||||
'webhook_url',
|
||||
'sms_phone_number',
|
||||
'opsgenie_api_key',
|
||||
'pagerduty_integration_key',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -47,9 +57,43 @@ class User extends Authenticatable
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'notification_preferences' => 'array',
|
||||
'notification_channels' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled notification channels for a specific alert type
|
||||
*/
|
||||
public function getNotificationChannelsFor(string $alertType): array
|
||||
{
|
||||
$channels = $this->notification_channels ?? [];
|
||||
return $channels[$alertType] ?? ['database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Route notification for Slack
|
||||
*/
|
||||
public function routeNotificationForSlack(): ?string
|
||||
{
|
||||
return $this->slack_webhook_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route notification for Telegram
|
||||
*/
|
||||
public function routeNotificationForTelegram(): ?string
|
||||
{
|
||||
return $this->telegram_chat_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route notification for Discord
|
||||
*/
|
||||
public function routeNotificationForDiscord(): ?string
|
||||
{
|
||||
return $this->discord_webhook_url;
|
||||
}
|
||||
|
||||
public function sites(): HasMany
|
||||
{
|
||||
return $this->hasMany(Site::class);
|
||||
|
||||
Reference in New Issue
Block a user