Initial commit

This commit is contained in:
Ronald Huynen
2026-03-23 21:37:59 +01:00
commit 2547717edb
2193 changed files with 972171 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
<?php
namespace App\Http\Livewire\Admin;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Livewire\Component;
class Log extends Component
{
public $logContent = '';
public $message = '';
public $diskUsage = '';
public $diskUsageClass = '';
public $availableRam;
public $availableRamClass;
public $queueWorkers = [];
public $queueWorkersCount = 0;
public $reverbConnected = false;
public function mount()
{
// Security check: user must be authenticated
if (!Auth::check()) {
abort(403, 'Unauthorized');
}
// Security check: activeProfileType must be 'App\Models\Admin'
if (getActiveProfileType() !== 'Admin') {
abort(403, 'Unauthorized');
}
// Security check: user must own the active profile
$activeProfile = getActiveProfile();
if (!$activeProfile) {
abort(403, 'Unauthorized');
}
// CRITICAL SECURITY: Validate user has ownership/access to this profile
\App\Helpers\ProfileAuthorizationHelper::authorize($activeProfile);
// Load log content - support both daily (laravel-YYYY-MM-DD.log) and single (laravel.log) drivers
$logPath = storage_path('logs/laravel-' . date('Y-m-d') . '.log');
if (!file_exists($logPath)) {
$logPath = storage_path('logs/laravel.log');
}
if (file_exists($logPath)) {
$logLines = (int) timebank_config('admin_settings.log_lines', 100);
$this->logContent = shell_exec("tail -n {$logLines} " . escapeshellarg($logPath));
// Use tail output to check for warnings/errors (avoid file() which loads entire file into memory)
$recentLines = explode("\n", $this->logContent ?? '');
$messages = [];
if (collect($recentLines)->contains(fn ($line) => stripos($line, 'WARNING') !== false)) {
$messages[] = '<span class="font-bold text-orange-500">Warning</span> detected in the recent log output';
}
if (collect($recentLines)->contains(fn ($line) => stripos($line, 'ERROR') !== false)) {
$messages[] = '<span class="font-bold text-red-500">Error</span> detected in the recent log output';
}
if (collect($recentLines)->contains(fn ($line) => stripos($line, 'CRITICAL') !== false)) {
$messages[] = '<span class="font-bold text-red-500">Critical</span> issue detected in the recent log output';
}
if (collect($recentLines)->contains(fn ($line) => stripos($line, 'ALERT') !== false)) {
$messages[] = '<span class="font-bold text-red-500">Alert</span> detected in the recent log output - IMMEDIATE ACTION REQUIRED';
}
$this->message = implode('<br>', $messages);
}
// Get disk usage
$free = disk_free_space("/");
$total = disk_total_space("/");
$used = $total - $free;
$percent = $total > 0 ? round(($used / $total) * 100, 1) : 0;
// Determine disk usage color class
if ($percent > 90) {
$this->diskUsageClass = 'text-red-500';
} elseif ($percent > 75) {
$this->diskUsageClass = 'text-orange-500';
} else {
$this->diskUsageClass = 'text-green-500';
}
$this->diskUsage = sprintf(
'%s used of %s (%.1f%%)',
$this->formatBytes($used),
$this->formatBytes($total),
$percent
);
// Get RAM memory information
$meminfo = file_get_contents('/proc/meminfo');
preg_match('/MemTotal:\s+(\d+)/', $meminfo, $matchesTotal);
preg_match('/MemAvailable:\s+(\d+)/', $meminfo, $matchesAvailable);
$totalKb = $matchesTotal[1] ?? 0;
$availableKb = $matchesAvailable[1] ?? 0;
$usedKb = $totalKb - $availableKb;
$percentRam = $totalKb > 0 ? round(($usedKb / $totalKb) * 100, 1) : 0;
$this->availableRam = sprintf(
'%s used of %s (%.1f%%)',
$this->formatBytes($usedKb * 1024),
$this->formatBytes($totalKb * 1024),
$percentRam
);
if ($percentRam > 90) {
$this->availableRamClass = 'text-red-500';
} elseif ($percentRam > 75) {
$this->availableRamClass = 'text-orange-500';
} else {
$this->availableRamClass = 'text-green-500';
}
// Check running queue workers
$queueWorkers = [];
exec("ps aux | grep 'artisan queue:work' | grep -v grep", $output);
foreach ($output as $line) {
// Parse $line for more details
$queueWorkers[] = $line;
}
$this->queueWorkers = $queueWorkers;
$this->queueWorkersCount = count($queueWorkers);
// Check Reverb server connection (example: check if port is open)
$reverbPort = env('REVERB_PORT', 6001);
// Check Reverb server connection
$reverbConnected = false;
$connection = @fsockopen('127.0.0.1', $reverbPort, $errno, $errstr, 1);
if ($connection) {
$reverbConnected = true;
fclose($connection);
}
$this->reverbConnected = $reverbConnected;
}
// Helper to format bytes
public function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = $bytes > 0 ? floor(log($bytes) / log(1024)) : 0;
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
public function downloadLog()
{
$logPath = storage_path('logs/laravel-' . date('Y-m-d') . '.log');
if (!file_exists($logPath)) {
$logPath = storage_path('logs/laravel.log');
}
if (file_exists($logPath)) {
return response()->download($logPath, 'laravel-' . date('Y-m-d') . '.log');
}
session()->flash('error', 'Log file not found');
}
public function render()
{
$layout = Auth::check() ? 'app-layout' : 'guest-layout';
return view('livewire.admin.log', [
'layout' => $layout,
'logContent' => $this->logContent,
'message' => $this->message,
'diskUsage' => $this->diskUsage,
'diskUsageClass' => $this->diskUsageClass,
'availableRam' => $this->availableRam,
'availableRamClass' => $this->availableRamClass,
'queueWorkers' => $this->queueWorkers,
'queueWorkersCount' => $this->queueWorkersCount,
'reverbConnected' => $this->reverbConnected,
]);
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Http\Livewire\Admin;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\File;
use Livewire\Component;
class LogViewer extends Component
{
public $logFilename;
public $logContent = '';
public $message = '';
public $fileSize = '';
public $lastModified = '';
public $logTitle = '';
public function mount($logFilename, $logTitle = null)
{
// Security check: user must be authenticated
if (!Auth::check()) {
abort(403, 'Unauthorized');
}
// Security check: activeProfileType must be 'App\Models\Admin'
if (getActiveProfileType() !== 'Admin') {
abort(403, 'Unauthorized');
}
// Security check: user must own the active profile
$activeProfile = getActiveProfile();
if (!$activeProfile) {
abort(403, 'Unauthorized');
}
// CRITICAL SECURITY: Validate user has ownership/access to this profile
\App\Helpers\ProfileAuthorizationHelper::authorize($activeProfile);
$this->logFilename = $logFilename;
$this->logTitle = $logTitle ?? $logFilename;
$this->loadLogContent();
}
public function loadLogContent()
{
$logPath = storage_path('logs/' . $this->logFilename);
// Security: prevent directory traversal attacks
$realPath = realpath($logPath);
$logsDir = realpath(storage_path('logs'));
if (!$realPath || strpos($realPath, $logsDir) !== 0) {
$this->message = '<span class="font-bold text-red-500">Error:</span> Invalid log file path';
return;
}
if (file_exists($logPath)) {
// Get file info
$this->fileSize = $this->formatBytes(filesize($logPath));
$this->lastModified = date('Y-m-d H:i:s', filemtime($logPath));
// Load log content using tail (avoid file() which loads entire file into memory)
$logLines = (int) timebank_config('admin_settings.log_lines', 100);
$this->logContent = shell_exec("tail -n {$logLines} " . escapeshellarg($logPath));
$recentLines = explode("\n", $this->logContent ?? '');
// Check for warnings/errors in log content
$messages = [];
if (collect($recentLines)->contains(fn ($line) => stripos($line, 'WARNING') !== false)) {
$messages[] = '<span class="font-bold text-orange-500">Warning</span> detected in the recent log output';
}
if (collect($recentLines)->contains(fn ($line) => stripos($line, 'ERROR') !== false)) {
$messages[] = '<span class="font-bold text-red-500">Error</span> detected in the recent log output';
}
if (collect($recentLines)->contains(fn ($line) => stripos($line, 'CRITICAL') !== false)) {
$messages[] = '<span class="font-bold text-red-500">Critical</span> issue detected in the recent log output';
}
if (collect($recentLines)->contains(fn ($line) => stripos($line, 'ALERT') !== false)) {
$messages[] = '<span class="font-bold text-red-500">Alert</span> detected in the recent log output - IMMEDIATE ACTION REQUIRED';
}
if (!empty($messages)) {
$this->message = implode('<br>', $messages);
}
} else {
$this->message = '<span class="font-bold text-gray-500">Log file not found or empty</span>';
}
}
public function downloadLog()
{
$logPath = storage_path('logs/' . $this->logFilename);
// Security: prevent directory traversal attacks
$realPath = realpath($logPath);
$logsDir = realpath(storage_path('logs'));
if (!$realPath || strpos($realPath, $logsDir) !== 0) {
session()->flash('error', 'Invalid log file path');
return;
}
if (file_exists($logPath)) {
return response()->download($logPath, $this->logFilename . '-' . date('Y-m-d') . '.log');
}
session()->flash('error', 'Log file not found');
}
public function refreshLog()
{
$this->loadLogContent();
$this->dispatch('logRefreshed');
}
// Helper to format bytes
public function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = $bytes > 0 ? floor(log($bytes) / log(1024)) : 0;
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
public function render()
{
return view('livewire.admin.log-viewer');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Livewire\Admin;
use Livewire\Attributes\On;
use Livewire\Component;
class MaintenanceBanner extends Component
{
public $maintenanceMode = false;
public function mount()
{
$this->maintenanceMode = isMaintenanceMode();
}
#[On('maintenance-mode-changed')]
public function refreshMaintenanceMode()
{
$this->maintenanceMode = isMaintenanceMode();
}
public function render()
{
return view('livewire.admin.maintenance-banner');
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\Http\Livewire\Admin;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Livewire\Component;
class MaintenanceMode extends Component
{
public $maintenanceMode = false;
public $showModal = false;
public function mount()
{
// Check if the active profile is Admin
if (getActiveProfileType() !== 'Admin') {
abort(403, 'Only administrators can access this feature.');
}
// Load current maintenance mode status
$this->maintenanceMode = $this->getMaintenanceMode();
}
public function openModal()
{
$this->showModal = true;
}
public function closeModal()
{
$this->showModal = false;
}
public function toggleMaintenanceMode()
{
// Verify admin profile again before toggling
if (getActiveProfileType() !== 'Admin') {
$this->dispatch('notify', [
'type' => 'error',
'message' => 'You must be logged in as an administrator to toggle maintenance mode.'
]);
return;
}
// Toggle the value
$this->maintenanceMode = !$this->maintenanceMode;
// If enabling maintenance mode, log out all non-admin users
if ($this->maintenanceMode) {
$this->logoutNonAdminUsers();
}
// Update in database
DB::table('system_settings')
->where('key', 'maintenance_mode')
->update([
'value' => $this->maintenanceMode ? 'true' : 'false',
'updated_at' => now(),
]);
// Clear cache
Cache::forget('system_setting_maintenance_mode');
// Close modal
$this->showModal = false;
// Dispatch event to refresh the maintenance banner
$this->dispatch('maintenance-mode-changed');
// Notify user
$message = $this->maintenanceMode
? 'Maintenance mode has been enabled. Only users with admin relationships can now log in.'
: 'Maintenance mode has been disabled. All users can now log in.';
$this->dispatch('notify', [
'type' => 'success',
'message' => $message
]);
}
/**
* Log out all users who don't have admin relationships
*/
protected function logoutNonAdminUsers()
{
// Get the current authenticated user ID to exclude from logout
$currentUserId = auth()->id();
// Get all users without admin relationships, excluding current user
$usersToLogout = \App\Models\User::whereDoesntHave('admins')
->where('id', '!=', $currentUserId)
->get();
$logoutCount = 0;
// First, broadcast logout events to all users
// This gives browsers a chance to receive the WebSocket message before sessions are deleted
foreach ($usersToLogout as $user) {
// Determine the guard for this user
$guard = 'web'; // Default guard
// Broadcast forced logout event via WebSocket
broadcast(new \App\Events\UserForcedLogout($user->id, $guard));
$logoutCount++;
}
// Wait briefly for WebSocket messages to be delivered
// This helps ensure browsers receive the logout event before sessions are deleted
sleep(2);
// Now delete sessions and clear caches
foreach ($usersToLogout as $user) {
$guard = 'web';
// Delete all sessions for this user from database
\DB::connection(config('session.connection'))
->table(config('session.table', 'sessions'))
->where('user_id', $user->id)
->delete();
// Clear cached authentication data
Cache::forget('auth_' . $guard . '_' . $user->id);
// Clear presence cache
Cache::forget("presence_{$guard}_{$user->id}");
}
// Clear online users cache to force refresh
Cache::forget("online_users_web_" . \App\Services\PresenceService::ONLINE_THRESHOLD_MINUTES);
// Log the action for debugging
info("Maintenance mode enabled: Logged out {$logoutCount} non-admin users. Current admin user ID {$currentUserId} was preserved.");
}
protected function getMaintenanceMode()
{
return isMaintenanceMode();
}
public function render()
{
return view('livewire.admin.maintenance-mode');
}
}