Explorar el Código

Merge remote-tracking branch 'origin/vpn-btn'

Luca Parisio hace 4 meses
padre
commit
d6545463d9

+ 204 - 0
app/Console/Commands/VpnStatusCommand.php

@@ -0,0 +1,204 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
+
+class VpnStatusCommand extends Command
+{
+    protected $signature = 'vpn:status {--refresh : Refresh status from system} {--sync : Sync database with system status}';
+    protected $description = 'Controlla e sincronizza lo stato della connessione VPN';
+
+    public function handle()
+    {
+        $this->info('=== VPN STATUS REPORT ===');
+        $this->newLine();
+
+        // 1. Stato dal database
+        $this->checkDatabaseStatus();
+
+        // 2. Stato dal sistema (se richiesto)
+        if ($this->option('refresh') || $this->option('sync')) {
+            $this->checkSystemStatus();
+        }
+
+        // 3. Sincronizzazione (se richiesta)
+        if ($this->option('sync')) {
+            $this->syncStatus();
+        }
+
+        // 4. Verifica file e configurazioni
+        $this->checkFiles();
+
+        // 5. Mostra log recenti
+        $this->showRecentLogs();
+
+        $this->newLine();
+        $this->info('=== END REPORT ===');
+    }
+
+    private function checkDatabaseStatus()
+    {
+        $this->line('<comment>1. Database Status:</comment>');
+
+        try {
+            $status = DB::table('vpn_status')->first();
+
+            if ($status) {
+                $statusColor = $status->status === 'connected' ? 'green' : 'red';
+                $this->line("   Status: <fg={$statusColor}>{$status->status}</>");
+                $this->line("   Last Update: {$status->last_update}");
+                $this->line("   Record ID: {$status->id}");
+            } else {
+                $this->error('   No status record found in database');
+            }
+        } catch (\Exception $e) {
+            $this->error('   Database error: ' . $e->getMessage());
+        }
+
+        $this->newLine();
+    }
+
+    private function syncStatus()
+    {
+        $this->line('<comment>3. Synchronizing Database with System:</comment>');
+
+        try {
+            // Ottieni stato dal sistema
+            $output = [];
+            exec('/opt/cisco/anyconnect/bin/vpn state 2>&1', $output);
+            $statusText = implode(' ', $output);
+
+            $systemStatus = 'unknown';
+            if (strpos($statusText, 'state: Connected') !== false) {
+                $systemStatus = 'connected';
+            } elseif (strpos($statusText, 'Disconnected') !== false) {
+                $systemStatus = 'disconnected';
+            }
+
+            // Ottieni stato dal database
+            $dbRecord = DB::table('vpn_status')->first();
+            $dbStatus = $dbRecord ? $dbRecord->status : 'no_record';
+
+            $this->line("   System Status: <fg=yellow>{$systemStatus}</>");
+            $this->line("   Database Status: <fg=yellow>{$dbStatus}</>");
+
+            if ($systemStatus !== 'unknown' && $systemStatus !== $dbStatus) {
+                // Aggiorna il database
+                DB::table('vpn_status')->updateOrInsert(
+                    ['id' => 1],
+                    [
+                        'status' => $systemStatus,
+                        'last_update' => now(),
+                        'updated_at' => now(),
+                    ]
+                );
+
+                $this->line("   ✅ <fg=green>Database updated to: {$systemStatus}</>");
+                Log::info('VPN status synced via command', [
+                    'old_status' => $dbStatus,
+                    'new_status' => $systemStatus
+                ]);
+            } else {
+                $this->line('   ℹ️  No sync needed - statuses match');
+            }
+
+        } catch (\Exception $e) {
+            $this->error("   ❌ Sync failed: " . $e->getMessage());
+        }
+    }
+
+    private function checkSystemStatus()
+    {
+        $this->line('<comment>2. System Status (Live Check):</comment>');
+
+        if (file_exists('/opt/cisco/anyconnect/bin/vpn')) {
+            $this->line('   ✅ Cisco AnyConnect: Installed');
+
+            $output = [];
+            $returnVar = 0;
+            exec('/opt/cisco/anyconnect/bin/vpn state 2>&1', $output, $returnVar);
+
+            $this->line('   System Status Output:');
+            foreach ($output as $line) {
+                $this->line('      ' . $line);
+            }
+
+            // Analizza l'output
+            $statusText = implode(' ', $output);
+            if (strpos($statusText, 'Connected') !== false) {
+                $this->line('   ✅ <fg=green>System Status: CONNECTED</>', 'green');
+            } elseif (strpos($statusText, 'Disconnected') !== false) {
+                $this->line('   ❌ <fg=red>System Status: DISCONNECTED</>');
+            } else {
+                $this->line('   ⚠️  <fg=yellow>System Status: UNKNOWN</>');
+            }
+        } else {
+            $this->error('   ❌ Cisco AnyConnect: Not Installed');
+        }
+
+        $this->newLine();
+    }
+
+    private function checkFiles()
+    {
+        $this->line('<comment>3. Configuration Files:</comment>');
+
+        $files = [
+            'scripts/vpn-connect.sh' => 'Connection Script',
+            'scripts/vpn-disconnect.sh' => 'Disconnection Script',
+            'scripts/vpn-config.conf' => 'Configuration File',
+        ];
+
+        foreach ($files as $file => $description) {
+            if (Storage::exists($file)) {
+                $this->line("   ✅ {$description}: Present");
+
+                // Mostra i permessi per gli script
+                if (str_ends_with($file, '.sh')) {
+                    $fullPath = storage_path($file);
+                    $perms = substr(sprintf('%o', fileperms($fullPath)), -4);
+                    $this->line("      Permissions: {$perms}");
+                }
+            } else {
+                $this->error("   ❌ {$description}: Missing");
+            }
+        }
+
+        // Controlla directory logs
+        if (Storage::exists('logs')) {
+            $this->line('   ✅ Logs Directory: Present');
+        } else {
+            $this->error('   ❌ Logs Directory: Missing');
+        }
+
+        $this->newLine();
+    }
+
+    private function showRecentLogs()
+    {
+        $this->line('<comment>4. Recent Log Entries:</comment>');
+
+        $logFile = storage_path('app/logs/vpn-connection.log');
+
+        if (file_exists($logFile)) {
+            $this->line("   Log file: {$logFile}");
+
+            // Leggi le ultime 10 righe
+            $lines = file($logFile);
+            $recentLines = array_slice($lines, -10);
+
+            $this->line('   Last 10 entries:');
+            foreach ($recentLines as $line) {
+                $this->line('      ' . trim($line));
+            }
+        } else {
+            $this->error('   Log file not found');
+        }
+
+        $this->newLine();
+    }
+}

+ 36 - 0
app/Console/Commands/VpnUpdateStatus.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class VpnUpdateStatus extends Command
+{
+    protected $signature = 'vpn:update-status {status}';
+    protected $description = 'Aggiorna lo stato della connessione VPN';
+
+    public function handle()
+    {
+        $status = $this->argument('status');
+
+        try {
+            DB::table('vpn_status')->updateOrInsert(
+                ['id' => 1],
+                [
+                    'status' => $status,
+                    'last_update' => now(),
+                    'updated_at' => now(),
+                ]
+            );
+
+            Log::info("Stato VPN aggiornato: {$status}");
+            $this->info("Stato VPN aggiornato: {$status}");
+
+        } catch (\Exception $e) {
+            Log::error("Errore aggiornamento stato VPN: " . $e->getMessage());
+            $this->error("Errore aggiornamento stato VPN: " . $e->getMessage());
+        }
+    }
+}

+ 213 - 0
app/Http/Livewire/VpnManagement.php

@@ -0,0 +1,213 @@
+<?php
+
+namespace App\Http\Livewire;
+
+use Livewire\Component;
+use App\Services\VpnManager;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Validation\ValidationException;
+
+class VpnManagement extends Component
+{
+    public $username = 'com.cmrmp013.0001';
+    public $password = '';
+    public $server = 'anyvpn.ilportaledellautomobilista.it/utentiMCTC';
+    public $vpnStatus = 'unknown';
+    public $lastUpdate = null;
+    public $successMessage = '';
+    public $errorMessage = '';
+
+    protected $rules = [
+        'username' => 'required|string',
+        'password' => 'required|string',
+        'server' => 'required|string',
+    ];
+
+    protected $vpnManager;
+
+    public function boot(VpnManager $vpnManager)
+    {
+        $this->vpnManager = $vpnManager;
+    }
+
+    public function mount()
+    {
+        // Al caricamento, forza una sincronizzazione con il sistema
+        $this->refreshStatus(true);
+    }
+
+    public function render()
+    {
+        return view('livewire.vpn-management');
+    }
+
+    public function refreshStatus($forceSystemCheck = false)
+    {
+        $this->vpnStatus = $this->vpnManager->getVpnStatus($forceSystemCheck);
+        $this->lastUpdate = $this->vpnManager->getLastUpdate();
+        $this->emit('vpnStatusUpdated', $this->vpnStatus);
+    }
+
+    public function syncWithSystem()
+    {
+        try {
+            $realStatus = $this->vpnManager->syncStatusWithSystem();
+            $this->vpnStatus = $realStatus;
+            $this->lastUpdate = $this->vpnManager->getLastUpdate();
+
+            $this->emit('vpnStatusUpdated', $this->vpnStatus);
+            $this->emit('showToast', 'success', 'Stato sincronizzato: ' . $this->getStatusText());
+
+            Log::info('VPN status manually synced from frontend', [
+                'new_status' => $realStatus
+            ]);
+
+        } catch (\Exception $e) {
+            $this->emit('showToast', 'error', 'Errore nella sincronizzazione: ' . $e->getMessage());
+            Log::error('Error syncing VPN status from frontend: ' . $e->getMessage());
+        }
+    }
+
+    public function connectVpn()
+    {
+        try {
+            // Se già connesso, non fare nulla
+            if ($this->vpnStatus === 'connected') {
+                $this->emit('showToast', 'info', 'VPN già connessa');
+                return;
+            }
+
+            $result = $this->vpnManager->connectVpn();
+
+            if ($result) {
+                $this->emit('showToast', 'success', 'Comando di connessione VPN inviato!');
+                Log::info('VPN connection command sent from frontend');
+
+                // Refresh status after a delay
+                $this->emit('refreshStatusDelayed');
+            } else {
+                $this->emit('showToast', 'error', 'Errore nel lancio della connessione VPN');
+                Log::error('Failed to send VPN connection command from frontend');
+            }
+        } catch (\Exception $e) {
+            $this->emit('showToast', 'error', 'Errore: ' . $e->getMessage());
+            Log::error('Error in connectVpn from frontend: ' . $e->getMessage());
+        }
+    }
+
+    public function disconnectVpn()
+    {
+        try {
+            // Se già disconnesso, non fare nulla
+            if ($this->vpnStatus === 'disconnected') {
+                $this->emit('showToast', 'info', 'VPN già disconnessa');
+                return;
+            }
+
+            $result = $this->vpnManager->disconnectVpn();
+
+            if ($result) {
+                $this->emit('showToast', 'success', 'Comando di disconnessione VPN inviato!');
+                Log::info('VPN disconnection command sent from frontend');
+
+                // Update status immediately for disconnect
+                $this->vpnStatus = 'disconnected';
+                $this->lastUpdate = now();
+                $this->emit('vpnStatusUpdated', $this->vpnStatus);
+
+            } else {
+                $this->emit('showToast', 'error', 'Errore nella disconnessione VPN');
+                Log::error('Failed to send VPN disconnection command from frontend');
+            }
+        } catch (\Exception $e) {
+            $this->emit('showToast', 'error', 'Errore: ' . $e->getMessage());
+            Log::error('Error in disconnectVpn from frontend: ' . $e->getMessage());
+        }
+    }
+
+    public function updateCredentials()
+    {
+        try {
+            $this->validate();
+
+            $success = $this->vpnManager->updateCredentials(
+                $this->username,
+                $this->password,
+                $this->server
+            );
+
+            if ($success) {
+                $this->successMessage = 'Credenziali VPN aggiornate con successo!';
+                $this->errorMessage = '';
+                $this->password = ''; // Clear password field for security
+
+                // Emit event for JavaScript notification
+                $this->emit('showToast', 'success', 'Credenziali VPN aggiornate con successo!');
+
+                Log::info('VPN credentials updated successfully');
+            } else {
+                $this->errorMessage = 'Errore nell\'aggiornamento delle credenziali VPN.';
+                $this->successMessage = '';
+                $this->emit('showToast', 'error', 'Errore nell\'aggiornamento delle credenziali VPN.');
+            }
+        } catch (ValidationException $e) {
+            // Validation errors are handled automatically by Livewire
+            $this->emit('showToast', 'error', 'Errori di validazione nei campi');
+        } catch (\Exception $e) {
+            $this->errorMessage = 'Errore: ' . $e->getMessage();
+            $this->successMessage = '';
+            $this->emit('showToast', 'error', 'Errore: ' . $e->getMessage());
+            Log::error('Error updating VPN credentials: ' . $e->getMessage());
+        }
+    }
+
+    public function getStatusBadgeClass()
+    {
+        return match($this->vpnStatus) {
+            'connected' => 'badge badge-success',
+            'disconnected' => 'badge badge-danger',
+            'error' => 'badge badge-danger',
+            default => 'badge badge-warning'
+        };
+    }
+
+    public function getStatusBadgeText()
+    {
+        return match($this->vpnStatus) {
+            'connected' => 'ON',
+            'disconnected' => 'OFF',
+            'error' => 'ERR',
+            default => '?'
+        };
+    }
+
+    public function getStatusIconClass()
+    {
+        return match($this->vpnStatus) {
+            'connected' => 'info-box-icon bg-success',
+            'disconnected' => 'info-box-icon bg-danger',
+            'error' => 'info-box-icon bg-danger',
+            default => 'info-box-icon bg-warning'
+        };
+    }
+
+    public function getStatusIcon()
+    {
+        return match($this->vpnStatus) {
+            'connected' => 'fas fa-check',
+            'disconnected' => 'fas fa-times',
+            'error' => 'fas fa-exclamation-triangle',
+            default => 'fas fa-question'
+        };
+    }
+
+    public function getStatusText()
+    {
+        return match($this->vpnStatus) {
+            'connected' => 'Connesso',
+            'disconnected' => 'Disconnesso',
+            'error' => 'Errore',
+            default => 'Sconosciuto'
+        };
+    }
+}

+ 3 - 1
app/Providers/AppServiceProvider.php

@@ -11,7 +11,9 @@ class AppServiceProvider extends ServiceProvider
      */
     public function register(): void
     {
-        //
+        $this->app->singleton(VpnManager::class, function ($app) {
+            return new VpnManager();
+        });
     }
 
     /**

+ 242 - 0
app/Services/VpnManager.php

@@ -0,0 +1,242 @@
+<?php
+
+namespace App\Services;
+
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Facades\Crypt;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Schema;
+
+class VpnManager
+{
+    private $configPath = 'scripts/vpn-config.conf';
+    private $scriptPath = 'scripts/vpn-connect.sh';
+
+    public function updateCredentials($username, $password, $server)
+    {
+        try {
+            $configContent = "# Configurazione VPN - storage/scripts/vpn-config.conf\n";
+            $configContent .= "# ATTENZIONE: Questo file deve avere permessi 600\n\n";
+            $configContent .= "VPN_USERNAME=\"{$username}\"\n";
+            $configContent .= "VPN_PASSWORD=\"{$password}\"\n";
+            $configContent .= "VPN_SERVER=\"{$server}\"\n";
+
+            Storage::put($this->configPath, $configContent);
+
+            $fullPath = storage_path( $this->configPath);
+            chmod($fullPath, 0600);
+
+            Log::info('Credenziali VPN aggiornate con successo');
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Errore nell\'aggiornamento delle credenziali VPN: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    public function getVpnStatus($forceRefresh = false)
+    {
+        try {
+            if ($forceRefresh) {
+                return $this->syncStatusWithSystem();
+            }
+
+            if (!Schema::hasTable('vpn_status')) {
+                Log::warning('Tabella vpn_status non esiste');
+                return 'disconnected';
+            }
+
+            $status = \DB::table('vpn_status')->first();
+
+            if (!$status) {
+                return $this->syncStatusWithSystem();
+            }
+
+            return $status->status;
+        } catch (\Exception $e) {
+            Log::error('Errore nel recupero dello stato VPN: ' . $e->getMessage());
+            return 'disconnected';
+        }
+    }
+
+    public function getLastUpdate()
+    {
+        try {
+            if (!Schema::hasTable('vpn_status')) {
+                return now();
+            }
+
+            $status = \DB::table('vpn_status')->first();
+            return $status ? $status->last_update : now();
+        } catch (\Exception $e) {
+            Log::error('Errore nel recupero dell\'ultimo aggiornamento VPN: ' . $e->getMessage());
+            return now();
+        }
+    }
+
+    public function checkRealVpnStatus()
+    {
+        try {
+            // Esegui il comando per verificare lo stato reale
+            $output = [];
+            $returnVar = 0;
+            exec('/opt/cisco/anyconnect/bin/vpn state 2>&1', $output, $returnVar);
+
+            $statusText = implode(' ', $output);
+
+            Log::info('VPN real status check', [
+                'output' => $output,
+                'return_var' => $returnVar,
+                'status_text' => $statusText
+            ]);
+
+            // Determina lo stato basandosi sull'output
+            if (
+                strpos($statusText, 'state: Connected') !== false ||
+                strpos($statusText, 'Connected') !== false
+            ) {
+                return 'connected';
+            } elseif (
+                strpos($statusText, 'Disconnected') !== false ||
+                strpos($statusText, 'state: Disconnected') !== false
+            ) {
+                return 'disconnected';
+            } else {
+                return 'unknown';
+            }
+        } catch (\Exception $e) {
+            Log::error('Error checking real VPN status: ' . $e->getMessage());
+            return 'error';
+        }
+    }
+
+    public function syncStatusWithSystem()
+    {
+        try {
+            $realStatus = $this->checkRealVpnStatus();
+
+            \DB::table('vpn_status')->updateOrInsert(
+                ['id' => 1],
+                [
+                    'status' => $realStatus,
+                    'last_update' => now(),
+                    'updated_at' => now(),
+                ]
+            );
+
+            Log::info('VPN status synced with system', [
+                'real_status' => $realStatus
+            ]);
+
+            return $realStatus;
+        } catch (\Exception $e) {
+            Log::error('Error syncing VPN status: ' . $e->getMessage());
+            return 'error';
+        }
+    }
+
+    public function connectVpn()
+    {
+        try {
+            $currentStatus = $this->checkRealVpnStatus();
+            if ($currentStatus === 'connected') {
+                Log::info('VPN already connected, no action needed');
+                return true;
+            }
+
+            $scriptPath = storage_path($this->scriptPath);
+            $configPath = storage_path($this->configPath);
+
+            // Check if both script and config exist
+            if (!file_exists($scriptPath)) {
+                Log::error('Script VPN non trovato: ' . $scriptPath);
+                return false;
+            }
+
+            if (!file_exists($configPath)) {
+                Log::error('Config VPN non trovato: ' . $configPath);
+                return false;
+            }
+
+            // Make sure script is executable
+            $this->makeScriptExecutable();
+
+            // Execute the script and capture output
+            $command = "timeout 60 bash {$scriptPath} 2>&1";
+            exec($command, $output, $returnVar);
+
+            Log::info('Comando VPN connect eseguito', [
+                'command' => $command,
+                'output' => $output,
+                'return_var' => $returnVar
+            ]);
+
+            // Wait a moment and check status
+            sleep(5);
+            $attempts = 0;
+            $maxAttempts = 12; // 1 minute total (5 seconds * 12)
+
+            while ($attempts < $maxAttempts) {
+                $newStatus = $this->checkRealVpnStatus();
+
+                if ($newStatus === 'connected') {
+                    $this->syncStatusWithSystem();
+                    Log::info('VPN connection successful after ' . ($attempts + 1) . ' attempts');
+                    return true;
+                }
+
+                sleep(5);
+                $attempts++;
+            }
+
+            Log::warning('VPN connection timeout or failed', [
+                'final_status' => $this->checkRealVpnStatus(),
+                'attempts' => $attempts
+            ]);
+
+            return false;
+        } catch (\Exception $e) {
+            Log::error('Errore nell\'esecuzione del comando VPN connect: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    public function disconnectVpn()
+    {
+        try {
+            $command = "/opt/cisco/anyconnect/bin/vpn disconnect > /dev/null 2>&1 &";
+            exec($command, $output, $returnVar);
+
+            \DB::table('vpn_status')->updateOrInsert(
+                ['id' => 1],
+                [
+                    'status' => 'disconnected',
+                    'last_update' => now(),
+                    'updated_at' => now(),
+                ]
+            );
+
+            Log::info('Comando VPN disconnect eseguito', [
+                'command' => $command,
+                'return_var' => $returnVar
+            ]);
+
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Errore nell\'esecuzione del comando VPN disconnect: ' . $e->getMessage());
+            return false;
+        }
+    }
+
+    public function makeScriptExecutable()
+    {
+        try {
+            $fullPath = storage_path($this->scriptPath);
+            chmod($fullPath, 0755);
+            return true;
+        } catch (\Exception $e) {
+            Log::error('Errore nell\'impostazione dei permessi script: ' . $e->getMessage());
+            return false;
+        }
+    }
+}

+ 24 - 0
database/migrations/2025_07_03_105719_creat_vpn_staus_table.php

@@ -0,0 +1,24 @@
+
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up()
+    {
+        Schema::create('vpn_status', function (Blueprint $table) {
+            $table->id();
+            $table->string('status')->default('disconnected');
+            $table->timestamp('last_update');
+            $table->timestamps();
+        });
+    }
+
+    public function down()
+    {
+        Schema::dropIfExists('vpn_status');
+    }
+};

+ 31 - 1
resources/views/layouts/app.blade.php

@@ -237,6 +237,15 @@
                     </a>
                 </li>
             </ul>
+                <li class="nav-item">
+                    <a href="/vpn" class="nav-link">
+                        <i class="nav-icon fas fa-shield-alt"></i>
+                        <p>
+                            Connessione VPN
+                            <span class="badge badge-info right" id="vpn-status-badge">...</span>
+                        </p>
+                    </a>
+                </li>
           </li>
 
 
@@ -323,7 +332,28 @@
 <script src="/plugins/overlayScrollbars/js/jquery.overlayScrollbars.min.js"></script>
 <!-- AdminLTE App -->
 <script src="/dist/js/adminlte.js"></script>
-
+<script>
+$(document).ready(function() {
+    // Aggiorna il badge VPN nella sidebar ogni 30 secondi
+    function updateVpnBadge() {
+        $.get('/vpn/status', function(data) {
+            const badge = $('#vpn-status-badge');
+            if (data.status === 'connected') {
+                badge.removeClass().addClass('badge badge-success right').text('ON');
+            } else if (data.status === 'disconnected') {
+                badge.removeClass().addClass('badge badge-danger right').text('OFF');
+            } else {
+                badge.removeClass().addClass('badge badge-warning right').text('?');
+            }
+        }).fail(function() {
+            $('#vpn-status-badge').removeClass().addClass('badge badge-danger right').text('ERR');
+        });
+    }
+
+    updateVpnBadge();
+    setInterval(updateVpnBadge, 30000);
+});
+</script>
 
 @livewireScripts
 

+ 448 - 0
resources/views/livewire/vpn-management.blade.php

@@ -0,0 +1,448 @@
+<!-- resources/views/livewire/vpn-management.blade.php -->
+<div>
+    <!-- Content Header -->
+    <div class="content-header">
+        <div class="container-fluid">
+            <div class="row mb-2">
+                <div class="col-sm-6">
+                    <h1 class="m-0">Gestione Connessione VPN</h1>
+                </div>
+                <div class="col-sm-6">
+                    <ol class="breadcrumb float-sm-right">
+                        <li class="breadcrumb-item"><a href="/dashboard">Home</a></li>
+                        <li class="breadcrumb-item active">VPN</li>
+                    </ol>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Main content -->
+    <div class="row">
+        <!-- Stato VPN -->
+        <div class="col-md-6">
+            <div class="card card-primary">
+                <div class="card-header">
+                    <h3 class="card-title">
+                        <i class="fas fa-shield-alt"></i> Stato Connessione VPN
+                    </h3>
+                    <div class="card-tools">
+                        <button type="button"
+                                class="btn btn-tool"
+                                wire:click="refreshStatus(false)"
+                                title="Aggiorna stato database">
+                            <i class="fas fa-sync"></i>
+                        </button>
+                        <button type="button"
+                                class="btn btn-tool"
+                                wire:click="syncWithSystem"
+                                title="Sincronizza con sistema">
+                            <i class="fas fa-sync-alt"></i>
+                        </button>
+                    </div>
+                </div>
+                <div class="card-body">
+                    <div class="row">
+                        <div class="col-md-6">
+                            <div class="info-box">
+                                <span class="{{ $this->getStatusIconClass() }}">
+                                    <i class="{{ $this->getStatusIcon() }}"></i>
+                                </span>
+                                <div class="info-box-content">
+                                    <span class="info-box-text">Stato VPN</span>
+                                    <span class="info-box-number">{{ $this->getStatusText() }}</span>
+                                    <span class="info-box-more">
+                                        @if($lastUpdate)
+                                            Ultimo aggiornamento: {{ \Carbon\Carbon::parse($lastUpdate)->format('d/m/Y H:i:s') }}
+                                        @else
+                                            Nessun aggiornamento disponibile
+                                        @endif
+                                    </span>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="col-md-6">
+                            <div class="text-center">
+                                @if($vpnStatus === 'connected')
+                                    <button type="button" class="btn btn-success btn-lg" disabled>
+                                        <i class="fas fa-check"></i> VPN Connessa
+                                    </button>
+                                    <br><br>
+                                    <button type="button"
+                                            class="btn btn-warning btn-lg"
+                                            wire:click="disconnectVpn"
+                                            wire:loading.attr="disabled"
+                                            wire:target="disconnectVpn">
+                                        <span wire:loading.remove wire:target="disconnectVpn">
+                                            <i class="fas fa-stop"></i> Disconnetti VPN
+                                        </span>
+                                        <span wire:loading wire:target="disconnectVpn">
+                                            <i class="fas fa-spinner fa-spin"></i> Disconnettendo...
+                                        </span>
+                                    </button>
+                                @elseif($vpnStatus === 'disconnected')
+                                    <button type="button"
+                                            class="btn btn-success btn-lg"
+                                            wire:click="connectVpn"
+                                            wire:loading.attr="disabled"
+                                            wire:target="connectVpn">
+                                        <span wire:loading.remove wire:target="connectVpn">
+                                            <i class="fas fa-play"></i> Connetti VPN
+                                        </span>
+                                        <span wire:loading wire:target="connectVpn">
+                                            <i class="fas fa-spinner fa-spin"></i> Connettendo...
+                                        </span>
+                                    </button>
+                                    <br><br>
+                                    <button type="button" class="btn btn-secondary btn-lg" disabled>
+                                        <i class="fas fa-times"></i> VPN Disconnessa
+                                    </button>
+                                @else
+                                    <button type="button"
+                                            class="btn btn-success btn-lg"
+                                            wire:click="connectVpn"
+                                            wire:loading.attr="disabled"
+                                            wire:target="connectVpn">
+                                        <span wire:loading.remove wire:target="connectVpn">
+                                            <i class="fas fa-play"></i> Connetti VPN
+                                        </span>
+                                        <span wire:loading wire:target="connectVpn">
+                                            <i class="fas fa-spinner fa-spin"></i> Connettendo...
+                                        </span>
+                                    </button>
+                                    <br><br>
+                                    <button type="button"
+                                            class="btn btn-warning btn-lg"
+                                            wire:click="disconnectVpn"
+                                            wire:loading.attr="disabled"
+                                            wire:target="disconnectVpn">
+                                        <span wire:loading.remove wire:target="disconnectVpn">
+                                            <i class="fas fa-stop"></i> Disconnetti VPN
+                                        </span>
+                                        <span wire:loading wire:target="disconnectVpn">
+                                            <i class="fas fa-spinner fa-spin"></i> Disconnettendo...
+                                        </span>
+                                    </button>
+                                @endif
+
+                                <br><br>
+                                <button type="button"
+                                        class="btn btn-info btn-sm"
+                                        wire:click="syncWithSystem"
+                                        wire:loading.attr="disabled"
+                                        wire:target="syncWithSystem"
+                                        title="Sincronizza stato con sistema">
+                                    <span wire:loading.remove wire:target="syncWithSystem">
+                                        <i class="fas fa-sync-alt"></i> Sincronizza
+                                    </span>
+                                    <span wire:loading wire:target="syncWithSystem">
+                                        <i class="fas fa-spinner fa-spin"></i> Sincronizzando...
+                                    </span>
+                                </button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Configurazione Credenziali -->
+        <div class="col-md-6">
+            <div class="card card-warning">
+                <div class="card-header">
+                    <h3 class="card-title">
+                        <i class="fas fa-cog"></i> Configurazione Credenziali
+                    </h3>
+                </div>
+                <div class="card-body">
+                    @if($successMessage)
+                        <div class="alert alert-success alert-dismissible">
+                            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+                            {{ $successMessage }}
+                        </div>
+                    @endif
+
+                    @if($errorMessage)
+                        <div class="alert alert-danger alert-dismissible">
+                            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
+                            {{ $errorMessage }}
+                        </div>
+                    @endif
+
+                    <form wire:submit.prevent="updateCredentials">
+                        <div class="form-group">
+                            <label for="username">Username</label>
+                            <input type="text"
+                                   class="form-control @error('username') is-invalid @enderror"
+                                   wire:model="username"
+                                   required>
+                            @error('username')
+                                <div class="invalid-feedback">{{ $message }}</div>
+                            @enderror
+                        </div>
+
+                        <div class="form-group">
+                            <label for="password">Password</label>
+                            <input type="password"
+                                   class="form-control @error('password') is-invalid @enderror"
+                                   wire:model="password"
+                                   placeholder="Inserisci la password"
+                                   required>
+                            @error('password')
+                                <div class="invalid-feedback">{{ $message }}</div>
+                            @enderror
+                        </div>
+
+                        <div class="form-group">
+                            <label for="server">Server VPN</label>
+                            <input type="text"
+                                   class="form-control @error('server') is-invalid @enderror"
+                                   wire:model="server"
+                                   required>
+                            @error('server')
+                                <div class="invalid-feedback">{{ $message }}</div>
+                            @enderror
+                        </div>
+
+                        <button type="submit"
+                                class="btn btn-primary"
+                                wire:loading.attr="disabled"
+                                wire:target="updateCredentials">
+                            <span wire:loading.remove wire:target="updateCredentials">
+                                <i class="fas fa-save"></i> Salva Credenziali
+                            </span>
+                            <span wire:loading wire:target="updateCredentials">
+                                <i class="fas fa-spinner fa-spin"></i> Salvando...
+                            </span>
+                        </button>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <!-- Log VPN -->
+    <div class="row mt-4">
+        <div class="col-md-12">
+            <div class="card card-info">
+                <div class="card-header">
+                    <h3 class="card-title">
+                        <i class="fas fa-file-alt"></i> Informazioni VPN
+                    </h3>
+                </div>
+                <div class="card-body">
+                    <div class="table-responsive">
+                        <table class="table table-striped">
+                            <thead>
+                                <tr>
+                                    <th>Proprietà</th>
+                                    <th>Valore</th>
+                                </tr>
+                            </thead>
+                            <tbody>
+                                <tr>
+                                    <td><strong>Stato Attuale</strong></td>
+                                    <td>
+                                        <span class="{{ $this->getStatusBadgeClass() }}">
+                                            {{ $this->getStatusBadgeText() }}
+                                        </span>
+                                        {{ $this->getStatusText() }}
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Server Configurato</strong></td>
+                                    <td>{{ $server }}</td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Username Configurato</strong></td>
+                                    <td>{{ $username }}</td>
+                                </tr>
+                                <tr>
+                                    <td><strong>Ultimo Aggiornamento</strong></td>
+                                    <td>
+                                        @if($lastUpdate)
+                                            {{ \Carbon\Carbon::parse($lastUpdate)->format('d/m/Y H:i:s') }}
+                                        @else
+                                            Non disponibile
+                                        @endif
+                                    </td>
+                                </tr>
+                            </tbody>
+                        </table>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+@push('css')
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
+@endpush
+
+@push('scripts')
+<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
+<script>
+    let loadingTimeouts = {};
+
+    // Funzione per forzare il reset degli stati di loading
+    function forceResetLoadingStates() {
+        // Reset loading indicators
+        document.querySelectorAll('[wire\\:loading]').forEach(el => {
+            el.style.display = 'none';
+        });
+
+        // Show normal buttons
+        document.querySelectorAll('[wire\\:loading\\.remove]').forEach(el => {
+            el.style.display = '';
+        });
+
+        // Re-enable buttons (except permanently disabled ones)
+        document.querySelectorAll('button[disabled]').forEach(btn => {
+            if (!btn.classList.contains('permanently-disabled')) {
+                btn.removeAttribute('disabled');
+            }
+        });
+
+        // Clear all timeouts
+        Object.values(loadingTimeouts).forEach(timeout => clearTimeout(timeout));
+        loadingTimeouts = {};
+
+        console.log('Loading states forcefully reset');
+    }
+
+    // Set timeout for each loading state
+    function setLoadingTimeout(target, duration = 8000) {
+        if (loadingTimeouts[target]) {
+            clearTimeout(loadingTimeouts[target]);
+        }
+
+        loadingTimeouts[target] = setTimeout(() => {
+            console.warn(`Loading timeout for ${target}, forcing reset`);
+            forceResetLoadingStates();
+
+            // Show error toast
+            if (typeof toastr !== 'undefined') {
+                toastr.error('Operazione completata (timeout raggiunto)');
+            }
+        }, duration);
+    }
+
+    // Clear timeout when loading finishes
+    function clearLoadingTimeout(target) {
+        if (loadingTimeouts[target]) {
+            clearTimeout(loadingTimeouts[target]);
+            delete loadingTimeouts[target];
+        }
+    }
+
+    document.addEventListener('DOMContentLoaded', function() {
+        // Livewire event listeners
+        window.addEventListener('livewire:load', function () {
+            console.log('Livewire loaded');
+
+            // Auto-refresh status every 30 seconds (only if not loading)
+            setInterval(function() {
+                if (Object.keys(loadingTimeouts).length === 0) {
+                    @this.call('refreshStatus', false);
+                }
+            }, 30000);
+
+            // Update sidebar badge when status changes
+            Livewire.on('vpnStatusUpdated', status => {
+                updateSidebarBadge(status);
+            });
+
+            // Show toast notifications
+            Livewire.on('showToast', (type, message) => {
+                if (typeof toastr !== 'undefined') {
+                    toastr[type](message);
+                } else {
+                    alert(type.toUpperCase() + ': ' + message);
+                }
+            });
+
+            // Refresh status with delay
+            Livewire.on('refreshStatusDelayed', () => {
+                setTimeout(function() {
+                    if (Object.keys(loadingTimeouts).length === 0) {
+                        @this.call('refreshStatus', false);
+                    }
+                }, 3000);
+            });
+        });
+
+        // Track when loading starts
+        window.addEventListener('livewire:loading:start', function(event) {
+            const target = event.detail.target || 'unknown';
+            console.log('Loading started for:', target);
+            setLoadingTimeout(target);
+        });
+
+        // Track when loading finishes
+        window.addEventListener('livewire:loading:finish', function(event) {
+            const target = event.detail.target || 'unknown';
+            console.log('Loading finished for:', target);
+            clearLoadingTimeout(target);
+        });
+
+        // Force reset after 10 seconds on page load
+        setTimeout(forceResetLoadingStates, 10000);
+
+        // Initialize sidebar badge
+        const currentStatus = @json($vpnStatus ?? 'unknown');
+        updateSidebarBadge(currentStatus);
+    });
+
+    function updateSidebarBadge(status) {
+        const badge = document.getElementById('vpn-status-badge');
+        if (badge) {
+            badge.className = '';
+            switch(status) {
+                case 'connected':
+                    badge.className = 'badge badge-success right';
+                    badge.textContent = 'ON';
+                    break;
+                case 'disconnected':
+                    badge.className = 'badge badge-danger right';
+                    badge.textContent = 'OFF';
+                    break;
+                case 'error':
+                    badge.className = 'badge badge-danger right';
+                    badge.textContent = 'ERR';
+                    break;
+                default:
+                    badge.className = 'badge badge-warning right';
+                    badge.textContent = '?';
+            }
+        }
+    }
+
+    // Emergency reset button (remove this in production)
+    function addEmergencyResetButton() {
+        if (document.getElementById('emergency-reset')) return;
+
+        const resetBtn = document.createElement('button');
+        resetBtn.id = 'emergency-reset';
+        resetBtn.innerHTML = '🔄 Reset Loading';
+        resetBtn.className = 'btn btn-danger btn-sm';
+        resetBtn.style.position = 'fixed';
+        resetBtn.style.top = '10px';
+        resetBtn.style.right = '10px';
+        resetBtn.style.zIndex = '9999';
+        resetBtn.onclick = forceResetLoadingStates;
+
+        document.body.appendChild(resetBtn);
+
+        // Auto-remove after 30 seconds
+        setTimeout(() => {
+            if (resetBtn.parentNode) {
+                resetBtn.parentNode.removeChild(resetBtn);
+            }
+        }, 30000);
+    }
+
+    // Add emergency button if loading states persist
+    setTimeout(addEmergencyResetButton, 5000);
+</script>
+@endpush

+ 2 - 0
routes/web.php

@@ -112,6 +112,8 @@ Route::group(['middleware' => 'auth'],function(){
     Route::get('/parte-macchina', \App\Http\Livewire\ParteMacchina::class);
     Route::get('/tipo-veicolo', \App\Http\Livewire\TipoVeicolo::class);
     Route::get('/users', \App\Http\Livewire\User::class);
+    Route::get('/vpn', \App\Http\Livewire\VpnManagement::class);
+
 
     Route::get('/reports', \App\Http\Livewire\Report::class);
     Route::get('/istat', \App\Http\Livewire\Istat::class);

+ 121 - 0
storage/scripts/check-vpn-status.sh

@@ -0,0 +1,121 @@
+#!/bin/bash
+
+# Script per testare la connessione VPN
+# Salva come: storage/scripts/test-vpn-connection.sh
+
+echo "=== VPN CONNECTION TEST ==="
+echo "Data: $(date)"
+echo ""
+
+SCRIPT_DIR="$(dirname "$0")"
+CONFIG_FILE="$SCRIPT_DIR/vpn-config.conf"
+
+# 1. Verifica prerequisiti
+echo "1. Checking prerequisites..."
+
+if [[ ! -f "/opt/cisco/anyconnect/bin/vpn" ]]; then
+    echo "   ❌ Cisco AnyConnect not found"
+    exit 1
+fi
+echo "   ✅ Cisco AnyConnect installed"
+
+if [[ ! -f "$CONFIG_FILE" ]]; then
+    echo "   ❌ VPN config file not found: $CONFIG_FILE"
+    exit 1
+fi
+echo "   ✅ VPN config file found"
+
+# 2. Carica configurazione
+echo ""
+echo "2. Loading configuration..."
+source "$CONFIG_FILE"
+
+if [[ -z "$VPN_USERNAME" || -z "$VPN_PASSWORD" || -z "$VPN_SERVER" ]]; then
+    echo "   ❌ Configuration incomplete"
+    echo "      Username: ${VPN_USERNAME:-[missing]}"
+    echo "      Password: ${VPN_PASSWORD:+[set]}${VPN_PASSWORD:-[missing]}"
+    echo "      Server: ${VPN_SERVER:-[missing]}"
+    exit 1
+fi
+
+echo "   ✅ Configuration loaded"
+echo "      Username: $VPN_USERNAME"
+echo "      Server: $VPN_SERVER"
+
+# 3. Test connettività server
+echo ""
+echo "3. Testing server connectivity..."
+SERVER_HOST=$(echo "$VPN_SERVER" | cut -d'/' -f1)
+
+if ping -c 3 "$SERVER_HOST" >/dev/null 2>&1; then
+    echo "   ✅ Server $SERVER_HOST is reachable"
+else
+    echo "   ⚠️  Server $SERVER_HOST ping failed (might be normal for VPN servers)"
+fi
+
+# 4. Controlla stato attuale
+echo ""
+echo "4. Checking current VPN status..."
+CURRENT_STATUS=$(/opt/cisco/anyconnect/bin/vpn state 2>/dev/null)
+echo "   Current status:"
+echo "$CURRENT_STATUS" | sed 's/^/      /'
+
+# 5. Test di connessione (solo se disconnesso)
+if echo "$CURRENT_STATUS" | grep -q "Disconnected"; then
+    echo ""
+    echo "5. Testing VPN connection..."
+    echo "   Attempting to connect (this may take 30-60 seconds)..."
+
+    # Crea un file temporaneo con le credenziali
+    TEMP_CREDS=$(mktemp)
+    echo -e "${VPN_USERNAME}\n${VPN_PASSWORD}\ny" > "$TEMP_CREDS"
+
+    # Tenta la connessione con timeout
+    timeout 60 /opt/cisco/anyconnect/bin/vpn -s connect "$VPN_SERVER" < "$TEMP_CREDS" >/dev/null 2>&1
+    CONNECT_RESULT=$?
+
+    # Pulisci il file temporaneo
+    rm -f "$TEMP_CREDS"
+
+    # Controlla il risultato
+    sleep 5  # Aspetta che la connessione si stabilizzi
+    NEW_STATUS=$(/opt/cisco/anyconnect/bin/vpn state 2>/dev/null)
+
+    if echo "$NEW_STATUS" | grep -q "Connected"; then
+        echo "   ✅ VPN connection successful!"
+        echo "   New status:"
+        echo "$NEW_STATUS" | sed 's/^/      /'
+
+        # Test di connettività attraverso VPN
+        echo ""
+        echo "6. Testing connectivity through VPN..."
+        if curl -s --max-time 10 https://httpbin.org/ip >/dev/null 2>&1; then
+            echo "   ✅ Internet connectivity through VPN: OK"
+
+            # Mostra IP pubblico
+            PUBLIC_IP=$(curl -s --max-time 5 https://httpbin.org/ip | grep -o '"origin":"[^"]*"' | cut -d'"' -f4)
+            echo "   Public IP: ${PUBLIC_IP:-[unable to detect]}"
+        else
+            echo "   ⚠️  Internet connectivity test failed"
+        fi
+
+        # Disconnetti dopo il test
+        echo ""
+        echo "7. Disconnecting test connection..."
+        /opt/cisco/anyconnect/bin/vpn disconnect >/dev/null 2>&1
+        sleep 3
+        echo "   ✅ Disconnected"
+
+    else
+        echo "   ❌ VPN connection failed"
+        echo "   Status after attempt:"
+        echo "$NEW_STATUS" | sed 's/^/      /'
+    fi
+
+else
+    echo ""
+    echo "5. Skipping connection test (VPN already connected or in unknown state)"
+fi
+
+echo ""
+echo "=== TEST COMPLETED ==="

+ 66 - 0
storage/scripts/monitor-vpn-logs.sh

@@ -0,0 +1,66 @@
+#!/bin/bash
+
+# Script per monitorare i log VPN in tempo reale
+# Salva come: storage/scripts/monitor-vpn-logs.sh
+# Uso: bash storage/scripts/monitor-vpn-logs.sh
+
+SCRIPT_DIR="$(dirname "$0")"
+LOG_FILE="$SCRIPT_DIR/../logs/vpn-connection.log"
+LARAVEL_LOG="/var/www/polizia/storage/logs/laravel.log"
+
+echo "=== VPN LOG MONITOR ==="
+echo "Data: $(date)"
+echo "Log file: $LOG_FILE"
+echo "Laravel log: $LARAVEL_LOG"
+echo ""
+echo "Premi Ctrl+C per fermare il monitoraggio"
+echo "==========================================="
+echo ""
+
+# Funzione per mostrare log colorati
+show_colored_log() {
+    while IFS= read -r line; do
+        if [[ $line == *"ERROR"* ]]; then
+            echo -e "\033[31m$line\033[0m"  # Rosso
+        elif [[ $line == *"SUCCESS"* ]]; then
+            echo -e "\033[32m$line\033[0m"  # Verde
+        elif [[ $line == *"INFO"* ]]; then
+            echo -e "\033[34m$line\033[0m"  # Blu
+        elif [[ $line == *"WARNING"* ]]; then
+            echo -e "\033[33m$line\033[0m"  # Giallo
+        else
+            echo "$line"
+        fi
+    done
+}
+
+# Crea il file di log se non esiste
+if [[ ! -f "$LOG_FILE" ]]; then
+    echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] Log monitoring started" >> "$LOG_FILE"
+fi
+
+# Mostra le ultime 20 righe esistenti
+if [[ -f "$LOG_FILE" ]]; then
+    echo "=== ULTIMI LOG ESISTENTI ==="
+    tail -20 "$LOG_FILE" | show_colored_log
+    echo ""
+    echo "=== MONITORING IN TEMPO REALE ==="
+fi
+
+# Monitora in tempo reale
+tail -f "$LOG_FILE" 2>/dev/null | show_colored_log &
+VPN_LOG_PID=$!
+
+# Se esiste anche il log Laravel, monitoralo per errori VPN
+if [[ -f "$LARAVEL_LOG" ]]; then
+    tail -f "$LARAVEL_LOG" 2>/dev/null | grep -E "(VPN|vpn)" | while read line; do
+        echo -e "\033[35m[LARAVEL] $line\033[0m"  # Magenta
+    done &
+    LARAVEL_LOG_PID=$!
+fi
+
+# Gestisci l'interruzione
+trap 'echo ""; echo "Stopping log monitoring..."; kill $VPN_LOG_PID 2>/dev/null; [[ -n $LARAVEL_LOG_PID ]] && kill $LARAVEL_LOG_PID 2>/dev/null; exit 0' INT
+
+# Mantieni lo script in esecuzione
+wait

+ 59 - 0
storage/scripts/sync-vpn-status.sh

@@ -0,0 +1,59 @@
+#!/bin/bash
+
+# Script per sincronizzare lo stato VPN con il database
+# Salva come: storage/scripts/sync-vpn-status.sh
+# Aggiungi al cron: */5 * * * * /var/www/polizia/storage/scripts/sync-vpn-status.sh
+
+SCRIPT_DIR="$(dirname "$0")"
+LARAVEL_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
+LOG_FILE="$SCRIPT_DIR/../logs/vpn-status-sync.log"
+
+# Funzione di logging
+log_message() {
+    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
+}
+
+# Controlla se AnyConnect è installato
+if [[ ! -f "/opt/cisco/anyconnect/bin/vpn" ]]; then
+    log_message "ERROR: Cisco AnyConnect not found"
+    exit 1
+fi
+
+# Ottieni lo stato reale del sistema
+VPN_OUTPUT=$(/opt/cisco/anyconnect/bin/vpn state 2>/dev/null)
+SYSTEM_STATUS="unknown"
+
+if echo "$VPN_OUTPUT" | grep -q "state: Connected"; then
+    SYSTEM_STATUS="connected"
+elif echo "$VPN_OUTPUT" | grep -q "Disconnected"; then
+    SYSTEM_STATUS="disconnected"
+fi
+
+log_message "INFO: System VPN status detected: $SYSTEM_STATUS"
+
+# Ottieni lo stato dal database
+cd "$LARAVEL_DIR"
+DB_STATUS=$(php artisan tinker --execute="echo \DB::table('vpn_status')->first()->status ?? 'no_record';" 2>/dev/null | tail -1)
+
+log_message "INFO: Database VPN status: $DB_STATUS"
+
+# Aggiorna il database se necessario
+if [[ "$SYSTEM_STATUS" != "$DB_STATUS" && "$SYSTEM_STATUS" != "unknown" ]]; then
+    log_message "INFO: Status mismatch detected. Updating database from '$DB_STATUS' to '$SYSTEM_STATUS'"
+
+    php artisan tinker --execute="
+        \DB::table('vpn_status')->updateOrInsert(
+            ['id' => 1],
+            [
+                'status' => '$SYSTEM_STATUS',
+                'last_update' => now(),
+                'updated_at' => now(),
+            ]
+        );
+        echo 'Database updated successfully';
+    " >> "$LOG_FILE" 2>&1
+
+    log_message "SUCCESS: Database status updated to: $SYSTEM_STATUS"
+else
+    log_message "INFO: Status already in sync: $SYSTEM_STATUS"
+fi

+ 121 - 0
storage/scripts/test-vpn-connection.sh

@@ -0,0 +1,121 @@
+#!/bin/bash
+
+# Script per testare la connessione VPN
+# Salva come: storage/scripts/test-vpn-connection.sh
+
+echo "=== VPN CONNECTION TEST ==="
+echo "Data: $(date)"
+echo ""
+
+SCRIPT_DIR="$(dirname "$0")"
+CONFIG_FILE="$SCRIPT_DIR/vpn-config.conf"
+
+# 1. Verifica prerequisiti
+echo "1. Checking prerequisites..."
+
+if [[ ! -f "/opt/cisco/anyconnect/bin/vpn" ]]; then
+    echo "   ❌ Cisco AnyConnect not found"
+    exit 1
+fi
+echo "   ✅ Cisco AnyConnect installed"
+
+if [[ ! -f "$CONFIG_FILE" ]]; then
+    echo "   ❌ VPN config file not found: $CONFIG_FILE"
+    exit 1
+fi
+echo "   ✅ VPN config file found"
+
+# 2. Carica configurazione
+echo ""
+echo "2. Loading configuration..."
+source "$CONFIG_FILE"
+
+if [[ -z "$VPN_USERNAME" || -z "$VPN_PASSWORD" || -z "$VPN_SERVER" ]]; then
+    echo "   ❌ Configuration incomplete"
+    echo "      Username: ${VPN_USERNAME:-[missing]}"
+    echo "      Password: ${VPN_PASSWORD:+[set]}${VPN_PASSWORD:-[missing]}"
+    echo "      Server: ${VPN_SERVER:-[missing]}"
+    exit 1
+fi
+
+echo "   ✅ Configuration loaded"
+echo "      Username: $VPN_USERNAME"
+echo "      Server: $VPN_SERVER"
+
+# 3. Test connettività server
+echo ""
+echo "3. Testing server connectivity..."
+SERVER_HOST=$(echo "$VPN_SERVER" | cut -d'/' -f1)
+
+if ping -c 3 "$SERVER_HOST" >/dev/null 2>&1; then
+    echo "   ✅ Server $SERVER_HOST is reachable"
+else
+    echo "   ⚠️  Server $SERVER_HOST ping failed (might be normal for VPN servers)"
+fi
+
+# 4. Controlla stato attuale
+echo ""
+echo "4. Checking current VPN status..."
+CURRENT_STATUS=$(/opt/cisco/anyconnect/bin/vpn state 2>/dev/null)
+echo "   Current status:"
+echo "$CURRENT_STATUS" | sed 's/^/      /'
+
+# 5. Test di connessione (solo se disconnesso)
+if echo "$CURRENT_STATUS" | grep -q "Disconnected"; then
+    echo ""
+    echo "5. Testing VPN connection..."
+    echo "   Attempting to connect (this may take 30-60 seconds)..."
+
+    # Crea un file temporaneo con le credenziali
+    TEMP_CREDS=$(mktemp)
+    echo -e "${VPN_USERNAME}\n${VPN_PASSWORD}\ny" > "$TEMP_CREDS"
+
+    # Tenta la connessione con timeout
+    timeout 60 /opt/cisco/anyconnect/bin/vpn -s connect "$VPN_SERVER" < "$TEMP_CREDS" >/dev/null 2>&1
+    CONNECT_RESULT=$?
+
+    # Pulisci il file temporaneo
+    rm -f "$TEMP_CREDS"
+
+    # Controlla il risultato
+    sleep 5  # Aspetta che la connessione si stabilizzi
+    NEW_STATUS=$(/opt/cisco/anyconnect/bin/vpn state 2>/dev/null)
+
+    if echo "$NEW_STATUS" | grep -q "Connected"; then
+        echo "   ✅ VPN connection successful!"
+        echo "   New status:"
+        echo "$NEW_STATUS" | sed 's/^/      /'
+
+        # Test di connettività attraverso VPN
+        echo ""
+        echo "6. Testing connectivity through VPN..."
+        if curl -s --max-time 10 https://httpbin.org/ip >/dev/null 2>&1; then
+            echo "   ✅ Internet connectivity through VPN: OK"
+
+            # Mostra IP pubblico
+            PUBLIC_IP=$(curl -s --max-time 5 https://httpbin.org/ip | grep -o '"origin":"[^"]*"' | cut -d'"' -f4)
+            echo "   Public IP: ${PUBLIC_IP:-[unable to detect]}"
+        else
+            echo "   ⚠️  Internet connectivity test failed"
+        fi
+
+        # Disconnetti dopo il test
+        echo ""
+        echo "7. Disconnecting test connection..."
+        /opt/cisco/anyconnect/bin/vpn disconnect >/dev/null 2>&1
+        sleep 3
+        echo "   ✅ Disconnected"
+
+    else
+        echo "   ❌ VPN connection failed"
+        echo "   Status after attempt:"
+        echo "$NEW_STATUS" | sed 's/^/      /'
+    fi
+
+else
+    echo ""
+    echo "5. Skipping connection test (VPN already connected or in unknown state)"
+fi
+
+echo ""
+echo "=== TEST COMPLETED ==="

+ 3 - 0
storage/scripts/vpn-config.conf

@@ -0,0 +1,3 @@
+VPN_USERNAME="com.cmrmp013.0001"
+VPN_PASSWORD="SuperQuindici-50"
+VPN_SERVER="anyvpn.ilportaledellautomobilista.it/utentiMCTC"

+ 42 - 0
storage/scripts/vpn-connect.sh

@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Script per connessione VPN Cisco AnyConnect
+# Posizionare in: storage/scripts/vpn-connect.sh
+
+# Legge le credenziali da file di configurazione
+source "$(dirname "$0")/vpn-config.conf"
+
+# Log file
+LOG_FILE="$(dirname "$0")/../logs/vpn-connection.log"
+
+# Funzione di logging
+log_message() {
+    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
+}
+
+# Verifica se le credenziali sono configurate
+if [[ -z "$VPN_USERNAME" || -z "$VPN_PASSWORD" || -z "$VPN_SERVER" ]]; then
+    log_message "ERROR: Credenziali VPN non configurate correttamente"
+    exit 1
+fi
+
+# Controlla se AnyConnect è installato
+if [[ ! -f "/opt/cisco/anyconnect/bin/vpn" ]]; then
+    log_message "ERROR: Cisco AnyConnect non trovato"
+    exit 1
+fi
+
+log_message "INFO: Avvio connessione VPN..."
+
+# Esegue la connessione VPN
+printf "${VPN_USERNAME}\n${VPN_PASSWORD}\ny" | /opt/cisco/anyconnect/bin/vpn -s connect "$VPN_SERVER"
+
+# Controlla il risultato
+if [[ $? -eq 0 ]]; then
+    log_message "SUCCESS: Connessione VPN stabilita"
+    # Aggiorna timestamp ultimo successo nel database Laravel
+    php "$(dirname "$0")/../../artisan" vpn:update-status connected
+else
+    log_message "ERROR: Connessione VPN fallita"
+    php "$(dirname "$0")/../../artisan" vpn:update-status failed
+fi

+ 33 - 0
storage/scripts/vpn-disconnect.sh

@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# Script per disconnessione VPN Cisco AnyConnect
+# Posizionare in: storage/scripts/vpn-disconnect.sh
+
+# Log file
+LOG_FILE="$(dirname "$0")/../logs/vpn-connection.log"
+
+# Funzione di logging
+log_message() {
+    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
+}
+
+# Controlla se AnyConnect è installato
+if [[ ! -f "/opt/cisco/anyconnect/bin/vpn" ]]; then
+    log_message "ERROR: Cisco AnyConnect non trovato"
+    exit 1
+fi
+
+log_message "INFO: Avvio disconnessione VPN..."
+
+# Esegue la disconnessione VPN
+/opt/cisco/anyconnect/bin/vpn disconnect
+
+# Controlla il risultato
+if [[ $? -eq 0 ]]; then
+    log_message "SUCCESS: Disconnessione VPN completata"
+    # Aggiorna timestamp nel database Laravel
+    php "$(dirname "$0")/../../artisan" vpn:update-status disconnected
+else
+    log_message "ERROR: Disconnessione VPN fallita"
+    php "$(dirname "$0")/../../artisan" vpn:update-status error
+fi