lots of progress
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckBrokenLinks;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
class MonitorLinksCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:links';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check for broken links on all enabled sites';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting broken link monitoring...');
|
||||
|
||||
// Fetch all sites with monitoring enabled
|
||||
$sites = Site::where('monitoring_enabled', true)->get();
|
||||
|
||||
if ($sites->isEmpty()) {
|
||||
$this->warn('No enabled sites found for broken link monitoring.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$sites->count()} enabled site(s) for broken link monitoring.");
|
||||
|
||||
// Create a batch of jobs to dispatch in parallel
|
||||
$jobs = $sites->map(function (Site $site) {
|
||||
return new CheckBrokenLinks($site);
|
||||
});
|
||||
|
||||
// Dispatch jobs using Laravel's bus with batch processing
|
||||
Bus::chain([
|
||||
function () use ($jobs) {
|
||||
Bus::batch($jobs->toArray())
|
||||
->name('Broken Link Monitoring')
|
||||
->allowFailures()
|
||||
->onQueue('monitoring')
|
||||
->dispatch();
|
||||
}
|
||||
])->dispatch();
|
||||
|
||||
$this->info("Dispatched {$jobs->count()} broken link check job(s).");
|
||||
$this->comment('Jobs are being processed in the queue...');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckSslCertificate;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
class MonitorSslCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:ssl';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check SSL certificates for all enabled sites';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting SSL certificate monitoring...');
|
||||
|
||||
// Fetch all sites with monitoring enabled
|
||||
$sites = Site::where('monitoring_enabled', true)->get();
|
||||
|
||||
if ($sites->isEmpty()) {
|
||||
$this->warn('No enabled sites found for SSL monitoring.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$sites->count()} enabled site(s) for SSL monitoring.");
|
||||
|
||||
// Create a batch of jobs to dispatch in parallel
|
||||
$jobs = $sites->map(function (Site $site) {
|
||||
return new CheckSslCertificate($site);
|
||||
});
|
||||
|
||||
// Dispatch jobs using Laravel's bus with batch processing
|
||||
Bus::chain([
|
||||
function () use ($jobs) {
|
||||
Bus::batch($jobs->toArray())
|
||||
->name('SSL Certificate Monitoring')
|
||||
->allowFailures()
|
||||
->onQueue('monitoring')
|
||||
->dispatch();
|
||||
}
|
||||
])->dispatch();
|
||||
|
||||
$this->info("Dispatched {$jobs->count()} SSL certificate check job(s).");
|
||||
$this->comment('Jobs are being processed in the queue...');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\CheckUptime;
|
||||
use App\Models\Site;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
class MonitorUptimeCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'monitor:uptime';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Check uptime for all enabled sites';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->info('Starting uptime monitoring...');
|
||||
|
||||
// Fetch all sites with monitoring enabled
|
||||
$sites = Site::where('monitoring_enabled', true)->get();
|
||||
|
||||
if ($sites->isEmpty()) {
|
||||
$this->warn('No enabled sites found for monitoring.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$this->info("Found {$sites->count()} enabled site(s) to monitor.");
|
||||
|
||||
// Create a batch of jobs to dispatch in parallel
|
||||
$jobs = $sites->map(function (Site $site) {
|
||||
return new CheckUptime($site);
|
||||
});
|
||||
|
||||
// Dispatch jobs using Laravel's bus with concurrency limit
|
||||
Bus::chain([
|
||||
function () use ($jobs) {
|
||||
// Using bus batch for parallel processing
|
||||
Bus::batch($jobs->toArray())
|
||||
->name('Uptime Monitoring')
|
||||
->allowFailures()
|
||||
->onQueue('monitoring')
|
||||
->dispatch();
|
||||
}
|
||||
])->dispatch();
|
||||
|
||||
$this->info("Dispatched {$jobs->count()} uptime check job(s).");
|
||||
$this->comment('Jobs are being processed in the queue...');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Notification;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AlertController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$notifications = auth()->user()->notifications()
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
$unreadCount = auth()->user()->notifications()
|
||||
->where('is_read', false)
|
||||
->count();
|
||||
|
||||
$readCount = auth()->user()->notifications()
|
||||
->where('is_read', true)
|
||||
->count();
|
||||
|
||||
return view('alerts.index', compact('notifications', 'unreadCount', 'readCount'));
|
||||
}
|
||||
|
||||
public function markAsRead(Notification $notification)
|
||||
{
|
||||
$this->authorize('view', $notification);
|
||||
|
||||
$notification->update(['is_read' => true]);
|
||||
|
||||
return redirect()->route('alerts.index')
|
||||
->with('success', 'Alert marked as read.');
|
||||
}
|
||||
|
||||
public function markAllAsRead()
|
||||
{
|
||||
auth()->user()->notifications()
|
||||
->where('is_read', false)
|
||||
->update(['is_read' => true]);
|
||||
|
||||
return redirect()->route('alerts.index')
|
||||
->with('success', 'All alerts marked as read.');
|
||||
}
|
||||
|
||||
public function destroy(Notification $notification)
|
||||
{
|
||||
$this->authorize('view', $notification);
|
||||
|
||||
$notification->delete();
|
||||
|
||||
return redirect()->route('alerts.index')
|
||||
->with('success', 'Alert deleted.');
|
||||
}
|
||||
|
||||
public function deleteRead()
|
||||
{
|
||||
auth()->user()->notifications()
|
||||
->where('is_read', true)
|
||||
->delete();
|
||||
|
||||
return redirect()->route('alerts.index')
|
||||
->with('success', 'All read alerts deleted.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ConfirmsPasswords;
|
||||
|
||||
class ConfirmPasswordController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Confirm Password Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller is responsible for handling password confirmations and
|
||||
| uses a simple trait to include the behavior. You're free to explore
|
||||
| this trait and override any functions that require customization.
|
||||
|
|
||||
*/
|
||||
|
||||
use ConfirmsPasswords;
|
||||
|
||||
/**
|
||||
* Where to redirect users when the intended url fails.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/home';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
|
||||
|
||||
class ForgotPasswordController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller is responsible for handling password reset emails and
|
||||
| includes a trait which assists in sending these notifications from
|
||||
| your application to your users. Feel free to explore this trait.
|
||||
|
|
||||
*/
|
||||
|
||||
use SendsPasswordResetEmails;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Login Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles authenticating users for the application and
|
||||
| redirecting them to your home screen. The controller uses a trait
|
||||
| to conveniently provide its functionality to your applications.
|
||||
|
|
||||
*/
|
||||
|
||||
use AuthenticatesUsers;
|
||||
|
||||
/**
|
||||
* Where to redirect users after login.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/dashboard';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest')->except('logout');
|
||||
$this->middleware('auth')->only('logout');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Register Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller handles the registration of new users as well as their
|
||||
| validation and creation. By default this controller uses a trait to
|
||||
| provide this functionality without requiring any additional code.
|
||||
|
|
||||
*/
|
||||
|
||||
use RegistersUsers;
|
||||
|
||||
/**
|
||||
* Where to redirect users after registration.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/dashboard';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a validator for an incoming registration request.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
|
||||
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user instance after a valid registration.
|
||||
*
|
||||
* @param array $data
|
||||
* @return \App\Models\User
|
||||
*/
|
||||
protected function create(array $data)
|
||||
{
|
||||
return User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\ResetsPasswords;
|
||||
|
||||
class ResetPasswordController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller is responsible for handling password reset requests
|
||||
| and uses a simple trait to include this behavior. You're free to
|
||||
| explore this trait and override any methods you wish to tweak.
|
||||
|
|
||||
*/
|
||||
|
||||
use ResetsPasswords;
|
||||
|
||||
/**
|
||||
* Where to redirect users after resetting their password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/dashboard';
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\VerifiesEmails;
|
||||
|
||||
class VerificationController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Email Verification Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller is responsible for handling email verification for any
|
||||
| user that recently registered with the application. Emails may also
|
||||
| be re-sent if the user didn't receive the original email message.
|
||||
|
|
||||
*/
|
||||
|
||||
use VerifiesEmails;
|
||||
|
||||
/**
|
||||
* Where to redirect users after verification.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/dashboard';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('signed')->only('verify');
|
||||
$this->middleware('throttle:6,1')->only('verify', 'resend');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
//
|
||||
use AuthorizesRequests, ValidatesRequests;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$sites = auth()->user()->sites()
|
||||
->with(['uptimeChecks' => function($query) {
|
||||
$query->latest('checked_at')->limit(1);
|
||||
}, 'sslCertificates' => function($query) {
|
||||
$query->latest('checked_at')->limit(1);
|
||||
}])
|
||||
->withCount('brokenLinks')
|
||||
->get();
|
||||
|
||||
// Calculate stats for each site
|
||||
$sites->each(function($site) {
|
||||
$checks = $site->uptimeChecks()
|
||||
->where('checked_at', '>=', now()->subDays(30))
|
||||
->get();
|
||||
|
||||
$totalChecks = $checks->count();
|
||||
$successfulChecks = $checks->where('status_code', '>=', 200)
|
||||
->where('status_code', '<', 400)
|
||||
->count();
|
||||
|
||||
$site->uptime_percentage = $totalChecks > 0 ? ($successfulChecks / $totalChecks) * 100 : 0;
|
||||
$site->avg_response_time = $checks->avg('response_time_ms');
|
||||
});
|
||||
|
||||
// Calculate overall stats
|
||||
$totalSites = $sites->count();
|
||||
$sitesUp = $sites->filter(function($site) {
|
||||
$latestCheck = $site->uptimeChecks->first();
|
||||
return $latestCheck && $latestCheck->status_code >= 200 && $latestCheck->status_code < 400;
|
||||
})->count();
|
||||
$sitesDown = $sites->filter(function($site) {
|
||||
$latestCheck = $site->uptimeChecks->first();
|
||||
return $latestCheck && ($latestCheck->status_code < 200 || $latestCheck->status_code >= 400);
|
||||
})->count();
|
||||
$avgUptime = $sites->avg('uptime_percentage') ?? 0;
|
||||
|
||||
// Prepare chart data for last 7 days
|
||||
$chartData = null;
|
||||
if ($sites->isNotEmpty()) {
|
||||
$labels = [];
|
||||
for ($i = 6; $i >= 0; $i--) {
|
||||
$labels[] = now()->subDays($i)->format('M d');
|
||||
}
|
||||
|
||||
$datasets = $sites->map(function($site) use ($labels) {
|
||||
$data = [];
|
||||
for ($i = 6; $i >= 0; $i--) {
|
||||
$date = now()->subDays($i)->startOfDay();
|
||||
$checks = $site->uptimeChecks()
|
||||
->whereDate('checked_at', $date)
|
||||
->get();
|
||||
|
||||
$totalChecks = $checks->count();
|
||||
$successfulChecks = $checks->where('status_code', '>=', 200)
|
||||
->where('status_code', '<', 400)
|
||||
->count();
|
||||
|
||||
$data[] = $totalChecks > 0 ? round(($successfulChecks / $totalChecks) * 100, 2) : null;
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => $site->name,
|
||||
'data' => $data,
|
||||
'color' => '#' . substr(md5($site->id), 0, 6)
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
$chartData = [
|
||||
'labels' => $labels,
|
||||
'datasets' => $datasets
|
||||
];
|
||||
}
|
||||
|
||||
return view('dashboard', compact(
|
||||
'sites',
|
||||
'totalSites',
|
||||
'sitesUp',
|
||||
'sitesDown',
|
||||
'avgUptime',
|
||||
'chartData'
|
||||
));
|
||||
}
|
||||
|
||||
public function show(Site $site)
|
||||
{
|
||||
$this->authorize('view', $site);
|
||||
|
||||
// Get uptime checks for the last 30 days
|
||||
$uptimeChecks = $site->uptimeChecks()
|
||||
->where('checked_at', '>=', now()->subDays(30))
|
||||
->orderBy('checked_at', 'asc')
|
||||
->get();
|
||||
|
||||
// Get latest SSL certificate
|
||||
$latestSslCert = $site->sslCertificates()
|
||||
->latest('checked_at')
|
||||
->first();
|
||||
|
||||
// Get broken links
|
||||
$brokenLinks = $site->brokenLinks()
|
||||
->orderBy('last_checked_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
// Calculate uptime percentage
|
||||
$totalChecks = $uptimeChecks->count();
|
||||
$successfulChecks = $uptimeChecks->where('status_code', '>=', 200)
|
||||
->where('status_code', '<', 400)
|
||||
->count();
|
||||
$uptimePercentage = $totalChecks > 0 ? round(($successfulChecks / $totalChecks) * 100, 2) : 0;
|
||||
|
||||
// Calculate average response time
|
||||
$avgResponseTime = $uptimeChecks->avg('response_time_ms');
|
||||
|
||||
// Prepare chart data
|
||||
$chartLabels = $uptimeChecks->pluck('checked_at')->map(fn($date) => $date->format('M d H:i'))->toArray();
|
||||
$chartData = $uptimeChecks->pluck('response_time_ms')->toArray();
|
||||
$chartStatusCodes = $uptimeChecks->pluck('status_code')->toArray();
|
||||
|
||||
return view('dashboard.show', compact(
|
||||
'site',
|
||||
'uptimeChecks',
|
||||
'latestSslCert',
|
||||
'brokenLinks',
|
||||
'uptimePercentage',
|
||||
'avgResponseTime',
|
||||
'chartLabels',
|
||||
'chartData',
|
||||
'chartStatusCodes'
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HomeController extends Controller
|
||||
{
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the application dashboard.
|
||||
*
|
||||
* @return \Illuminate\Contracts\Support\Renderable
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('home');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$preferences = auth()->user()->notification_preferences ?? [
|
||||
'uptime' => ['mail'],
|
||||
'ssl' => ['mail'],
|
||||
'broken_links' => ['mail']
|
||||
];
|
||||
|
||||
$slackWebhook = env('SLACK_WEBHOOK_URL', '');
|
||||
|
||||
return view('settings', compact('preferences', 'slackWebhook'));
|
||||
}
|
||||
|
||||
public function updateNotifications(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'uptime' => 'nullable|array',
|
||||
'uptime.*' => 'in:mail,slack',
|
||||
'ssl' => 'nullable|array',
|
||||
'ssl.*' => 'in:mail,slack',
|
||||
'broken_links' => 'nullable|array',
|
||||
'broken_links.*' => 'in:mail,slack',
|
||||
]);
|
||||
|
||||
$preferences = [
|
||||
'uptime' => $request->input('uptime', []),
|
||||
'ssl' => $request->input('ssl', []),
|
||||
'broken_links' => $request->input('broken_links', []),
|
||||
];
|
||||
|
||||
auth()->user()->update([
|
||||
'notification_preferences' => $preferences
|
||||
]);
|
||||
|
||||
return redirect()->route('settings')
|
||||
->with('success', 'Notification preferences updated successfully.');
|
||||
}
|
||||
|
||||
public function updateSlack(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'slack_webhook' => 'nullable|url'
|
||||
]);
|
||||
|
||||
// Note: In a production app, you would store this in the user's settings or env
|
||||
// For now, we'll just return a message
|
||||
return redirect()->route('settings')
|
||||
->with('success', 'Slack webhook URL saved. Please add it to your .env file as SLACK_WEBHOOK_URL.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Site;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$sites = Auth::user()->sites()
|
||||
->withCount(['uptimeChecks', 'sslCertificates', 'brokenLinks'])
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
// Get latest uptime check for each site
|
||||
$sites->each(function ($site) {
|
||||
$site->latestUptimeCheck = $site->uptimeChecks()->latest('checked_at')->first();
|
||||
$site->latestSslCertificate = $site->sslCertificates()->latest('checked_at')->first();
|
||||
});
|
||||
|
||||
return view('sites.index', compact('sites'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('sites.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'url' => 'required|url|max:255',
|
||||
'monitoring_enabled' => 'sometimes|nullable|in:on,off',
|
||||
]);
|
||||
|
||||
$site = Auth::user()->sites()->create([
|
||||
'name' => $validated['name'],
|
||||
'url' => $validated['url'],
|
||||
'monitoring_enabled' => $request->has('monitoring_enabled'),
|
||||
]);
|
||||
|
||||
return redirect()->route('sites.index')
|
||||
->with('success', 'Site added successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Site $site)
|
||||
{
|
||||
$this->authorize('view', $site);
|
||||
|
||||
return redirect()->route('dashboard.show', $site);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit(Site $site)
|
||||
{
|
||||
$this->authorize('update', $site);
|
||||
|
||||
return view('sites.edit', compact('site'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, Site $site)
|
||||
{
|
||||
$this->authorize('update', $site);
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'url' => 'required|url|max:255',
|
||||
'monitoring_enabled' => 'boolean',
|
||||
]);
|
||||
|
||||
$site->update([
|
||||
'name' => $validated['name'],
|
||||
'url' => $validated['url'],
|
||||
'monitoring_enabled' => $request->has('monitoring_enabled'),
|
||||
]);
|
||||
|
||||
return redirect()->route('sites.index')
|
||||
->with('success', 'Site updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy(Site $site)
|
||||
{
|
||||
$this->authorize('delete', $site);
|
||||
|
||||
$site->delete();
|
||||
|
||||
return redirect()->route('sites.index')
|
||||
->with('success', 'Site deleted successfully!');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\BrokenLink;
|
||||
use App\Models\Site;
|
||||
use App\Notifications\BrokenLinkAlert;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Spatie\Crawler\Crawler;
|
||||
use Spatie\Crawler\CrawlObservers\CrawlObserver;
|
||||
|
||||
class CheckBrokenLinks implements ShouldQueue
|
||||
{
|
||||
use Queueable, Batchable;
|
||||
|
||||
public $timeout = 300; // 5 minutes timeout for crawling
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Site $site
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
Log::info("Starting broken link check for site {$this->site->id} ({$this->site->url})");
|
||||
|
||||
// Parse the base URL to get the domain
|
||||
$parsedUrl = parse_url($this->site->url);
|
||||
|
||||
if (!isset($parsedUrl['host'])) {
|
||||
Log::warning("Invalid URL for broken link check on site {$this->site->id}: {$this->site->url}");
|
||||
return;
|
||||
}
|
||||
|
||||
$baseDomain = $parsedUrl['host'];
|
||||
|
||||
// Create a crawler observer to handle broken links
|
||||
$observer = new class($this->site, $baseDomain) extends CrawlObserver {
|
||||
private Site $site;
|
||||
private string $baseDomain;
|
||||
|
||||
public function __construct(Site $site, string $baseDomain)
|
||||
{
|
||||
$this->site = $site;
|
||||
$this->baseDomain = $baseDomain;
|
||||
}
|
||||
|
||||
public function crawled(
|
||||
UriInterface $url,
|
||||
ResponseInterface $response,
|
||||
?UriInterface $foundOnUrl = null,
|
||||
?string $linkText = null
|
||||
): void {
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
// Check if the link is broken (4xx or 5xx)
|
||||
if ($statusCode >= 400) {
|
||||
// Only log internal links (same domain)
|
||||
$linkHost = $url->getHost();
|
||||
|
||||
if ($linkHost === $this->baseDomain) {
|
||||
BrokenLink::updateOrCreate(
|
||||
[
|
||||
'site_id' => $this->site->id,
|
||||
'url' => (string) $url,
|
||||
],
|
||||
[
|
||||
'status_code' => $statusCode,
|
||||
'last_checked_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
Log::info("Broken link detected on site {$this->site->id}: {$url} (HTTP {$statusCode})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function crawlFailed(
|
||||
UriInterface $url,
|
||||
RequestException $requestException,
|
||||
?UriInterface $foundOnUrl = null,
|
||||
?string $linkText = null
|
||||
): void {
|
||||
// Only log internal links (same domain)
|
||||
$linkHost = $url->getHost();
|
||||
|
||||
if ($linkHost === $this->baseDomain) {
|
||||
$statusCode = $requestException->getResponse()
|
||||
? $requestException->getResponse()->getStatusCode()
|
||||
: null;
|
||||
|
||||
BrokenLink::updateOrCreate(
|
||||
[
|
||||
'site_id' => $this->site->id,
|
||||
'url' => (string) $url,
|
||||
],
|
||||
[
|
||||
'status_code' => $statusCode,
|
||||
'last_checked_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
Log::info("Crawl failed for site {$this->site->id}: {$url}");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Configure and start the crawler
|
||||
Crawler::create()
|
||||
->setConcurrency(3)
|
||||
->setMaximumDepth(3)
|
||||
->setMaximumCrawlCount(50)
|
||||
->ignoreRobots()
|
||||
->acceptNofollowLinks()
|
||||
->setCrawlObserver($observer)
|
||||
->startCrawling($this->site->url);
|
||||
|
||||
Log::info("Completed broken link check for site {$this->site->id}");
|
||||
|
||||
// Get all broken links for this site from current scan
|
||||
$brokenLinks = BrokenLink::where('site_id', $this->site->id)
|
||||
->where('last_checked_at', '>=', now()->subMinutes(10))
|
||||
->get()
|
||||
->map(fn($link) => [
|
||||
'url' => $link->url,
|
||||
'status_code' => $link->status_code,
|
||||
])
|
||||
->toArray();
|
||||
|
||||
// Send notification if broken links were found
|
||||
if (!empty($brokenLinks)) {
|
||||
$this->site->user->notify(new BrokenLinkAlert(
|
||||
$this->site,
|
||||
$brokenLinks
|
||||
));
|
||||
|
||||
Log::info("Broken link alert sent for site {$this->site->id}: " . count($brokenLinks) . " broken links found");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Broken link check failed for site {$this->site->id}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Notification;
|
||||
use App\Models\Site;
|
||||
use App\Models\SslCertificate;
|
||||
use App\Notifications\SslExpiryAlert;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CheckSslCertificate implements ShouldQueue
|
||||
{
|
||||
use Queueable, Batchable;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Site $site
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
// Parse URL to get host
|
||||
$parsedUrl = parse_url($this->site->url);
|
||||
|
||||
if (!isset($parsedUrl['host'])) {
|
||||
Log::warning("Invalid URL for SSL check on site {$this->site->id}: {$this->site->url}");
|
||||
return;
|
||||
}
|
||||
|
||||
$host = $parsedUrl['host'];
|
||||
$port = $parsedUrl['scheme'] === 'https' ? 443 : 443;
|
||||
|
||||
// Create SSL context
|
||||
$context = stream_context_create([
|
||||
'ssl' => [
|
||||
'capture_peer_cert' => true,
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
// Connect to the server and get SSL certificate
|
||||
$client = @stream_socket_client(
|
||||
"ssl://{$host}:{$port}",
|
||||
$errno,
|
||||
$errstr,
|
||||
10,
|
||||
STREAM_CLIENT_CONNECT,
|
||||
$context
|
||||
);
|
||||
|
||||
if ($client === false) {
|
||||
Log::warning("Failed to connect to {$host}:{$port} for SSL check: {$errstr}");
|
||||
return;
|
||||
}
|
||||
|
||||
$params = stream_context_get_params($client);
|
||||
fclose($client);
|
||||
|
||||
if (!isset($params['options']['ssl']['peer_certificate'])) {
|
||||
Log::warning("No SSL certificate found for site {$this->site->id}");
|
||||
return;
|
||||
}
|
||||
|
||||
$cert = openssl_x509_parse($params['options']['ssl']['peer_certificate']);
|
||||
|
||||
if ($cert === false) {
|
||||
Log::warning("Failed to parse SSL certificate for site {$this->site->id}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract certificate information
|
||||
$issuer = $cert['issuer']['O'] ?? $cert['issuer']['CN'] ?? 'Unknown';
|
||||
$expiresAt = Carbon::createFromTimestamp($cert['validTo_time_t']);
|
||||
$checkedAt = now();
|
||||
|
||||
// Store the SSL certificate check result
|
||||
SslCertificate::create([
|
||||
'site_id' => $this->site->id,
|
||||
'issuer' => $issuer,
|
||||
'expires_at' => $expiresAt,
|
||||
'checked_at' => $checkedAt,
|
||||
]);
|
||||
|
||||
// Check if certificate expires within 30 days
|
||||
$daysUntilExpiry = $checkedAt->diffInDays($expiresAt, false);
|
||||
|
||||
if ($daysUntilExpiry < 30 && $daysUntilExpiry >= 0) {
|
||||
// Send notification to user
|
||||
$this->site->user->notify(new SslExpiryAlert(
|
||||
$this->site,
|
||||
$issuer,
|
||||
$expiresAt,
|
||||
(int) $daysUntilExpiry,
|
||||
false
|
||||
));
|
||||
|
||||
Log::warning("SSL certificate expiring soon for site {$this->site->id}: {$daysUntilExpiry} days remaining");
|
||||
} elseif ($daysUntilExpiry < 0) {
|
||||
// Certificate already expired
|
||||
$this->site->user->notify(new SslExpiryAlert(
|
||||
$this->site,
|
||||
$issuer,
|
||||
$expiresAt,
|
||||
(int) $daysUntilExpiry,
|
||||
true
|
||||
));
|
||||
|
||||
Log::error("SSL certificate expired for site {$this->site->id}");
|
||||
}
|
||||
|
||||
Log::info("SSL check completed for site {$this->site->id} ({$this->site->url}): Issuer={$issuer}, Expires={$expiresAt->format('Y-m-d')}");
|
||||
} catch (\Exception $e) {
|
||||
Log::error("SSL check failed for site {$this->site->id}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Models\UptimeCheck;
|
||||
use App\Notifications\UptimeAlert;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
|
||||
class CheckUptime implements ShouldQueue
|
||||
{
|
||||
use Queueable, Batchable;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Site $site
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$client = new Client([
|
||||
'timeout' => 5,
|
||||
'connect_timeout' => 5,
|
||||
'allow_redirects' => [
|
||||
'max' => 5,
|
||||
'track_redirects' => true,
|
||||
],
|
||||
'http_errors' => false, // Don't throw exceptions on 4xx/5xx responses
|
||||
]);
|
||||
|
||||
$statusCode = null;
|
||||
$responseTime = null;
|
||||
$startTime = microtime(true);
|
||||
|
||||
|
||||
try {
|
||||
// Validate URL format
|
||||
if (!filter_var($this->site->url, FILTER_VALIDATE_URL)) {
|
||||
Log::warning("Invalid URL format for site {$this->site->id}: {$this->site->url}");
|
||||
$statusCode = null;
|
||||
$responseTime = null;
|
||||
|
||||
} else {
|
||||
// Send HEAD request for faster checks
|
||||
$response = $client->head($this->site->url);
|
||||
|
||||
$endTime = microtime(true);
|
||||
$responseTime = (int) round(($endTime - $startTime) * 1000); // Convert to milliseconds
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
}
|
||||
} catch (ConnectException $e) {
|
||||
// Connection timeout or DNS failure
|
||||
Log::info("Connection failed for site {$this->site->id}: {$e->getMessage()}");
|
||||
$statusCode = null;
|
||||
$responseTime = null;
|
||||
|
||||
} catch (RequestException $e) {
|
||||
// HTTP error occurred
|
||||
$endTime = microtime(true);
|
||||
$responseTime = (int) round(($endTime - $startTime) * 1000);
|
||||
$statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : null;
|
||||
|
||||
Log::info("Request failed for site {$this->site->id}: HTTP {$statusCode}");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// Any other exception
|
||||
Log::error("Unexpected error checking site {$this->site->id}: {$e->getMessage()}");
|
||||
$statusCode = null;
|
||||
$responseTime = null;
|
||||
|
||||
}
|
||||
|
||||
// Store the uptime check result
|
||||
UptimeCheck::create([
|
||||
'site_id' => $this->site->id,
|
||||
'status_code' => $statusCode,
|
||||
'response_time_ms' => $responseTime,
|
||||
'checked_at' => now(),
|
||||
]);
|
||||
|
||||
|
||||
// Send notification if site is down (status >= 400 or unreachable)
|
||||
if ($statusCode === null || $statusCode >= 400) {
|
||||
$this->site->user->notify(new UptimeAlert(
|
||||
$this->site,
|
||||
$statusCode,
|
||||
$responseTime
|
||||
));
|
||||
|
||||
Log::warning("Downtime alert sent for site {$this->site->id}: HTTP {$statusCode}");
|
||||
}
|
||||
Log::info("Uptime check completed for site {$this->site->id} ({$this->site->url}): HTTP {$statusCode}, {$responseTime}ms");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BrokenLink extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'url',
|
||||
'status_code',
|
||||
'last_checked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_checked_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'type',
|
||||
'message',
|
||||
'is_read',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_read' => 'boolean',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Site extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'url',
|
||||
'name',
|
||||
'monitoring_enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'monitoring_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function uptimeChecks(): HasMany
|
||||
{
|
||||
return $this->hasMany(UptimeCheck::class);
|
||||
}
|
||||
|
||||
public function sslCertificates(): HasMany
|
||||
{
|
||||
return $this->hasMany(SslCertificate::class);
|
||||
}
|
||||
|
||||
public function brokenLinks(): HasMany
|
||||
{
|
||||
return $this->hasMany(BrokenLink::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class SslCertificate extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'issuer',
|
||||
'expires_at',
|
||||
'checked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'checked_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UptimeCheck extends Model
|
||||
{
|
||||
const UPDATED_AT = null;
|
||||
|
||||
protected $fillable = [
|
||||
'site_id',
|
||||
'status_code',
|
||||
'response_time_ms',
|
||||
'checked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'checked_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function site(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Site::class);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
@@ -21,6 +22,7 @@ class User extends Authenticatable
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'notification_preferences',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -43,6 +45,17 @@ class User extends Authenticatable
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'notification_preferences' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function sites(): HasMany
|
||||
{
|
||||
return $this->hasMany(Site::class);
|
||||
}
|
||||
|
||||
public function notifications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Notification::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Site;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class BrokenLinkAlert extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Site $site,
|
||||
public array $brokenLinks
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$preferences = $notifiable->notification_preferences ?? [];
|
||||
$channels = $preferences['broken_links'] ?? ['mail'];
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$brokenLinkCount = count($this->brokenLinks);
|
||||
|
||||
$message = (new MailMessage)
|
||||
->warning()
|
||||
->subject("⚠️ Broken Links Detected: {$this->site->name}")
|
||||
->line("We detected **{$brokenLinkCount}** broken link(s) on **{$this->site->name}**.")
|
||||
->line("Site URL: {$this->site->url}")
|
||||
->line('')
|
||||
->line('**Broken Links:**');
|
||||
|
||||
// Add up to 10 broken links to the email
|
||||
$displayLimit = min(10, $brokenLinkCount);
|
||||
foreach (array_slice($this->brokenLinks, 0, $displayLimit) as $link) {
|
||||
$message->line("• {$link['url']} (HTTP {$link['status_code']})");
|
||||
}
|
||||
|
||||
if ($brokenLinkCount > $displayLimit) {
|
||||
$remaining = $brokenLinkCount - $displayLimit;
|
||||
$message->line("... and {$remaining} more");
|
||||
}
|
||||
|
||||
$message->line('')
|
||||
->action('View All Broken Links', url("/sites/{$this->site->id}/broken-links"))
|
||||
->line('Please fix these broken links to improve user experience and SEO.');
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'site_id' => $this->site->id,
|
||||
'site_name' => $this->site->name,
|
||||
'site_url' => $this->site->url,
|
||||
'broken_link_count' => count($this->brokenLinks),
|
||||
'broken_links' => $this->brokenLinks,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Site;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Messages\SlackMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class SslExpiryAlert extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Site $site,
|
||||
public string $issuer,
|
||||
public Carbon $expiresAt,
|
||||
public int $daysRemaining,
|
||||
public bool $isExpired = false
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$preferences = $notifiable->notification_preferences ?? [];
|
||||
$channels = $preferences['ssl'] ?? ['mail'];
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$message = (new MailMessage)
|
||||
->subject($this->isExpired
|
||||
? "🔴 SSL Certificate Expired: {$this->site->name}"
|
||||
: "⚠️ SSL Certificate Expiring Soon: {$this->site->name}")
|
||||
->line($this->isExpired
|
||||
? "The SSL certificate for **{$this->site->name}** has **expired**."
|
||||
: "The SSL certificate for **{$this->site->name}** is expiring soon.")
|
||||
->line("URL: {$this->site->url}")
|
||||
->line("Issuer: {$this->issuer}")
|
||||
->line($this->isExpired
|
||||
? "Expired on: {$this->expiresAt->format('Y-m-d')}"
|
||||
: "Expires on: {$this->expiresAt->format('Y-m-d')} ({$this->daysRemaining} days remaining)")
|
||||
->action('View Site Details', url("/sites/{$this->site->id}"));
|
||||
|
||||
if ($this->isExpired) {
|
||||
$message->error();
|
||||
$message->line('⚠️ Your site visitors may see security warnings. Please renew the certificate immediately.');
|
||||
} else {
|
||||
$message->warning();
|
||||
$message->line('Please renew the certificate before it expires to avoid service disruption.');
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Slack representation of the notification.
|
||||
*/
|
||||
public function toSlack(object $notifiable): SlackMessage
|
||||
{
|
||||
$color = $this->isExpired ? 'danger' : 'warning';
|
||||
$emoji = $this->isExpired ? '🔴' : '⚠️';
|
||||
|
||||
return (new SlackMessage)
|
||||
->from('Site Monitor')
|
||||
->to(config('services.slack.channel'))
|
||||
->content($this->isExpired
|
||||
? "{$emoji} SSL Certificate Expired!"
|
||||
: "{$emoji} SSL Certificate Expiring Soon!")
|
||||
->attachment(function ($attachment) use ($color) {
|
||||
$attachment
|
||||
->color($color)
|
||||
->title($this->site->name, url("/sites/{$this->site->id}"))
|
||||
->fields([
|
||||
'URL' => $this->site->url,
|
||||
'Issuer' => $this->issuer,
|
||||
'Expires' => $this->expiresAt->format('Y-m-d'),
|
||||
'Status' => $this->isExpired
|
||||
? 'EXPIRED'
|
||||
: "{$this->daysRemaining} days remaining",
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'site_id' => $this->site->id,
|
||||
'site_name' => $this->site->name,
|
||||
'site_url' => $this->site->url,
|
||||
'issuer' => $this->issuer,
|
||||
'expires_at' => $this->expiresAt->toDateTimeString(),
|
||||
'days_remaining' => $this->daysRemaining,
|
||||
'is_expired' => $this->isExpired,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Site;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class UptimeAlert extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new notification instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Site $site,
|
||||
public ?int $statusCode,
|
||||
public ?int $responseTime
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
$preferences = $notifiable->notification_preferences ?? [];
|
||||
$channels = $preferences['uptime'] ?? ['mail'];
|
||||
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*/
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
$status = $this->statusCode ? "returned HTTP {$this->statusCode}" : "is unreachable";
|
||||
|
||||
return (new MailMessage)
|
||||
->error()
|
||||
->subject("⚠️ Site Down Alert: {$this->site->name}")
|
||||
->line("Your site **{$this->site->name}** is experiencing downtime.")
|
||||
->line("URL: {$this->site->url}")
|
||||
->line("Status: {$status}")
|
||||
->line("Response Time: " . ($this->responseTime ? "{$this->responseTime}ms" : "N/A"))
|
||||
->line("Detected at: " . now()->format('Y-m-d H:i:s'))
|
||||
->action('View Site Details', url("/sites/{$this->site->id}"))
|
||||
->line('Please investigate the issue as soon as possible.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array representation of the notification.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'site_id' => $this->site->id,
|
||||
'site_name' => $this->site->name,
|
||||
'site_url' => $this->site->url,
|
||||
'status_code' => $this->statusCode,
|
||||
'response_time' => $this->responseTime,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Notification;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class NotificationPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Notification $notification): bool
|
||||
{
|
||||
return $user->id === $notification->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Notification $notification): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class SitePolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, Site $site): bool
|
||||
{
|
||||
return $user->id === $site->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, Site $site): bool
|
||||
{
|
||||
return $user->id === $site->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, Site $site): bool
|
||||
{
|
||||
return $user->id === $site->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*/
|
||||
public function restore(User $user, Site $site): bool
|
||||
{
|
||||
return $user->id === $site->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*/
|
||||
public function forceDelete(User $user, Site $site): bool
|
||||
{
|
||||
return $user->id === $site->user_id;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user