1352 lines
90 KiB
Plaintext
1352 lines
90 KiB
Plaintext
<x-proxmox::layout :server="$server" view="firewall" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
|
|
@if(!$status)
|
|
<x-proxmox::ui.alert variant="danger" icon="ri-error-warning-fill" class="mt-8">
|
|
<x-slot name="title">Unable to Load Server Status</x-slot>
|
|
<p class="text-gray-600 dark:text-gray-400 mb-3">Could not retrieve status from hypervisor:</p>
|
|
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 mb-3 space-y-1 text-sm">
|
|
<li>Temporary connectivity issues with the Proxmox cluster</li>
|
|
<li>The VM may be in an intermediate state</li>
|
|
<li>Network timeout while fetching data</li>
|
|
</ul>
|
|
<x-proxmox::ui.button wire:click="loadVmStatus" icon="ri-refresh-line">
|
|
Retry
|
|
</x-proxmox::ui.button>
|
|
</x-proxmox::ui.alert>
|
|
@else
|
|
|
|
{{-- Global Loader Styles --}}
|
|
<style>
|
|
.fw-loader {
|
|
width: 24px;
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
padding: 3px;
|
|
background:
|
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 2px)/10px 5px,
|
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 3px), currentColor calc(100% - 2px) 99%, #0000) top right/50% 50% content-box content-box,
|
|
radial-gradient(farthest-side at top, #0000 calc(100% - 3px), currentColor calc(100% - 2px) 99%, #0000) bottom/100% 50% content-box content-box;
|
|
background-repeat: no-repeat;
|
|
animation: fw-loader-spin 1s infinite linear;
|
|
}
|
|
.fw-loader-sm {
|
|
width: 16px;
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
padding: 2px;
|
|
background:
|
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 1.5px)/6px 3px,
|
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) top right/50% 50% content-box content-box,
|
|
radial-gradient(farthest-side at top, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) bottom/100% 50% content-box content-box;
|
|
background-repeat: no-repeat;
|
|
animation: fw-loader-spin 1s infinite linear;
|
|
}
|
|
.fw-loader-lg {
|
|
width: 32px;
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
padding: 4px;
|
|
background:
|
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 3px)/12px 6px,
|
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 4px), currentColor calc(100% - 3px) 99%, #0000) top right/50% 50% content-box content-box,
|
|
radial-gradient(farthest-side at top, #0000 calc(100% - 4px), currentColor calc(100% - 3px) 99%, #0000) bottom/100% 50% content-box content-box;
|
|
background-repeat: no-repeat;
|
|
animation: fw-loader-spin 1s infinite linear;
|
|
}
|
|
.fw-loader-xl {
|
|
width: 50px;
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
padding: 6px;
|
|
background:
|
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 4px)/17px 8.5px,
|
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 6px), currentColor calc(100% - 5px) 99%, #0000) top right/50% 50% content-box content-box,
|
|
radial-gradient(farthest-side at top, #0000 calc(100% - 6px), currentColor calc(100% - 5px) 99%, #0000) bottom/100% 50% content-box content-box;
|
|
background-repeat: no-repeat;
|
|
animation: fw-loader-spin 1s infinite linear;
|
|
}
|
|
@keyframes fw-loader-spin {
|
|
100% { transform: rotate(1turn); }
|
|
}
|
|
|
|
/* Firewall Toggle Switch */
|
|
.fw-toggle {
|
|
position: relative;
|
|
width: 64px;
|
|
height: 32px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.fw-toggle input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.fw-toggle-slider {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
|
border-radius: 32px;
|
|
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(220, 38, 38, 0.3);
|
|
}
|
|
.fw-toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 24px;
|
|
height: 24px;
|
|
left: 4px;
|
|
bottom: 4px;
|
|
background: linear-gradient(135deg, #ffffff 0%, #f3f4f6 100%);
|
|
border-radius: 50%;
|
|
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
}
|
|
.fw-toggle input:checked + .fw-toggle-slider {
|
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(34, 197, 94, 0.3);
|
|
}
|
|
.fw-toggle input:checked + .fw-toggle-slider::before {
|
|
transform: translateX(32px);
|
|
}
|
|
.fw-toggle:hover .fw-toggle-slider {
|
|
filter: brightness(1.1);
|
|
}
|
|
.fw-toggle input:disabled + .fw-toggle-slider {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
.fw-toggle-label {
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
transition: color 0.3s ease;
|
|
}
|
|
.fw-toggle-label-enabled {
|
|
color: #22c55e;
|
|
}
|
|
.fw-toggle-label-disabled {
|
|
color: #dc2626;
|
|
}
|
|
@keyframes fw-toggle-pulse {
|
|
0%, 100% { box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(34, 197, 94, 0.3); }
|
|
50% { box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 16px rgba(34, 197, 94, 0.5); }
|
|
}
|
|
.fw-toggle input:checked + .fw-toggle-slider {
|
|
animation: fw-toggle-pulse 2s infinite ease-in-out;
|
|
}
|
|
|
|
/* Small Toggle for Network Adapters */
|
|
.fw-toggle-sm {
|
|
position: relative;
|
|
width: 44px;
|
|
height: 24px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.fw-toggle-sm input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.fw-toggle-slider-sm {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
|
border-radius: 24px;
|
|
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2), 0 1px 6px rgba(220, 38, 38, 0.3);
|
|
}
|
|
.fw-toggle-slider-sm::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 18px;
|
|
height: 18px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background: linear-gradient(135deg, #ffffff 0%, #f3f4f6 100%);
|
|
border-radius: 50%;
|
|
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
box-shadow: 0 1px 6px rgba(0,0,0,0.2);
|
|
}
|
|
.fw-toggle-sm input:checked + .fw-toggle-slider-sm {
|
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2), 0 1px 6px rgba(34, 197, 94, 0.3);
|
|
}
|
|
.fw-toggle-sm input:checked + .fw-toggle-slider-sm::before {
|
|
transform: translateX(20px);
|
|
}
|
|
.fw-toggle-sm:hover .fw-toggle-slider-sm {
|
|
filter: brightness(1.1);
|
|
}
|
|
.fw-toggle-sm input:disabled + .fw-toggle-slider-sm {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|
|
|
|
<div class="space-y-6" x-data="{ pageLoaded: false, activeTab: 'rules' }" x-init="setTimeout(() => pageLoaded = true, 50)">
|
|
|
|
{{-- Page Header --}}
|
|
<div :class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'" style="transition: all 0.4s ease-out;">
|
|
<x-proxmox::ui.section-header
|
|
title="Firewall Management"
|
|
icon="ri-shield-line"
|
|
subtitle="Configure firewall rules, policies, aliases, and IPSets for your virtual machine."
|
|
class="mb-0">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.help-modal title="Firewall Management" icon="ri-question-line">
|
|
<p class="text-sm text-base/90 mb-4">
|
|
The Firewall Management interface provides comprehensive control over your virtual machine's network security, including rule-based filtering, IP aliases, and security policies.
|
|
</p>
|
|
<div class="pt-4 border-t border-neutral/30 space-y-2">
|
|
<p class="text-xs text-base/75"><span class="font-semibold text-base/90">Firewall Rules:</span> Create granular rules to allow or block specific traffic patterns.</p>
|
|
<p class="text-xs text-base/75"><span class="font-semibold text-base/90">IP Aliases:</span> Define named groups of IP addresses for easier rule management.</p>
|
|
<p class="text-xs text-base/75"><span class="font-semibold text-base/90">IP Sets:</span> Manage collections of IP addresses for advanced filtering.</p>
|
|
<p class="text-xs text-base/75"><span class="font-semibold text-base/90">Security Logs:</span> Monitor firewall activity and blocked attempts.</p>
|
|
</div>
|
|
</x-proxmox::ui.help-modal>
|
|
</x-slot>
|
|
</x-proxmox::ui.section-header>
|
|
</div>
|
|
|
|
{{-- Error Modal --}}
|
|
@if($error && $showErrorModal)
|
|
<x-proxmox::ui.modal title="Error" icon="ri-error-warning-line" icon-class="text-error" :show="true" wire:key="error-modal">
|
|
<x-slot name="header">
|
|
<button wire:click="$set('showErrorModal', false)" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</x-slot>
|
|
<p class="text-sm text-base/70">{{ $error }}</p>
|
|
<x-slot name="footer">
|
|
<x-proxmox::ui.button wire:click="$set('showErrorModal', false)" variant="secondary">
|
|
Close
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.modal>
|
|
@endif
|
|
|
|
{{-- Master Firewall Control --}}
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-sm transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'" style="transition: all 0.4s ease-out 0.1s;">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-shield-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Master Firewall Control</h2>
|
|
</div>
|
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-full {{ $status->status === 'running' ? 'bg-success/15 text-success' : 'bg-warning/15 text-warning' }}">
|
|
@if($status->status === 'running')
|
|
<span class="w-1.5 h-1.5 bg-success rounded-full animate-pulse"></span>
|
|
Running
|
|
@else
|
|
<span class="w-1.5 h-1.5 bg-warning rounded-full"></span>
|
|
Stopped
|
|
@endif
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-12 h-12 rounded-xl bg-primary flex items-center justify-center">
|
|
<x-ri-shield-line class="size-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-bold">Firewall Status</h2>
|
|
<p class="text-sm mt-1">
|
|
@if($firewallEnabled)
|
|
<span class="inline-flex items-center gap-1.5 text-success font-medium">
|
|
<span class="relative flex h-2 w-2">
|
|
<span class="absolute inline-flex h-full w-full rounded-full bg-success opacity-75 animate-ping"></span>
|
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-success"></span>
|
|
</span>
|
|
Active - Your VM is protected
|
|
</span>
|
|
@else
|
|
<span class="inline-flex items-center gap-1.5 text-warning font-medium">
|
|
<x-ri-error-warning-line class="size-4" />
|
|
Disabled - No firewall protection
|
|
</span>
|
|
@endif
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<span class="fw-toggle-label {{ $firewallEnabled ? 'fw-toggle-label-enabled' : 'fw-toggle-label-disabled' }}">
|
|
{{ $firewallEnabled ? 'Enabled' : 'Disabled' }}
|
|
</span>
|
|
<label class="fw-toggle" wire:loading.class="pointer-events-none" wire:click="toggleFirewall">
|
|
<input
|
|
type="checkbox"
|
|
wire:loading.attr="disabled"
|
|
{{ $firewallEnabled ? 'checked' : '' }}
|
|
readonly
|
|
>
|
|
<span class="fw-toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
<span wire:loading wire:target="toggleFirewall" class="text-xs text-base/50 flex items-center gap-1.5">
|
|
<span class="fw-loader-sm text-primary"></span>
|
|
{{ $firewallEnabled ? 'Disabling...' : 'Enabling...' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
@if($firewallEnabled)
|
|
{{-- Firewall Configuration --}}
|
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
|
|
|
{{-- Default Policies --}}
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'" style="transition: all 0.4s ease-out 0.15s;">
|
|
<div class="flex items-center gap-2 px-5 py-4 border-b border-gray-100 dark:border-gray-800">
|
|
<x-ri-filter-3-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Default Policies</h2>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<x-proxmox::ui.select
|
|
wire:model="policyIn"
|
|
label="Incoming Traffic (Policy In)"
|
|
help="Default action for incoming traffic not matching any rule">
|
|
<option value="ACCEPT">Accept</option>
|
|
<option value="REJECT">Reject</option>
|
|
<option value="DROP">Drop</option>
|
|
</x-proxmox::ui.select>
|
|
|
|
<x-proxmox::ui.select
|
|
wire:model="policyOut"
|
|
label="Outgoing Traffic (Policy Out)"
|
|
help="Default action for outgoing traffic not matching any rule">
|
|
<option value="ACCEPT">Accept</option>
|
|
<option value="REJECT">Reject</option>
|
|
<option value="DROP">Drop</option>
|
|
</x-proxmox::ui.select>
|
|
|
|
<x-proxmox::ui.button
|
|
wire:click="updatePolicies"
|
|
wire:loading.attr="disabled"
|
|
variant="success"
|
|
class="w-full mt-4">
|
|
<span wire:loading.remove wire:target="updatePolicies">Update Policies</span>
|
|
<span wire:loading wire:target="updatePolicies">Updating...</span>
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</section>
|
|
|
|
{{-- Network Adapters --}}
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'" style="transition: all 0.4s ease-out 0.2s;">
|
|
<div class="flex items-center gap-2 px-5 py-4 border-b border-gray-100 dark:border-gray-800">
|
|
<x-ri-router-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Network Adapters</h2>
|
|
</div>
|
|
|
|
<div class="p-5">
|
|
@if(count($networkAdapters) > 0)
|
|
<div class="space-y-3">
|
|
@foreach($networkAdapters as $adapter)
|
|
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="font-semibold text-sm">{{ $adapter['name'] }}</span>
|
|
<x-proxmox::ui.badge :variant="($adapterFirewallStates[$adapter['name']] ?? false) ? 'success' : 'secondary'">
|
|
{{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'Active' : 'Off' }}
|
|
</x-proxmox::ui.badge>
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ ucfirst($adapter['model'] ?? 'Unknown') }}
|
|
@if($adapter['mac'])
|
|
<span class="ml-2">{{ $adapter['mac'] }}</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span class="fw-toggle-label {{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'fw-toggle-label-enabled' : 'fw-toggle-label-disabled' }}">
|
|
{{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'On' : 'Off' }}
|
|
</span>
|
|
<label class="fw-toggle-sm" wire:loading.class="pointer-events-none" wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')">
|
|
<input
|
|
type="checkbox"
|
|
wire:loading.attr="disabled"
|
|
wire:key="adapter-{{ $adapter['name'] }}"
|
|
{{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'checked' : '' }}
|
|
readonly
|
|
>
|
|
<span class="fw-toggle-slider-sm"></span>
|
|
</label>
|
|
<span wire:loading wire:target="toggleAdapterFirewall('{{ $adapter['name'] }}')" class="text-xs text-base/50">
|
|
<span class="fw-loader-sm text-primary"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-router-line"
|
|
title="No Network Adapters"
|
|
description="No network adapters found on this VM" />
|
|
@endif
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{{-- Firewall Management Tabs --}}
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'" style="transition: all 0.4s ease-out 0.25s;">
|
|
<div class="flex border-b border-gray-100 dark:border-gray-800 overflow-x-auto">
|
|
<button @click="activeTab = 'rules'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'rules' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-list-check class="size-4" />
|
|
<span>Firewall Rules</span>
|
|
@if(count($rules) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($rules) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'rules'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
<button @click="activeTab = 'aliases'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'aliases' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-bookmark-line class="size-4" />
|
|
<span>IP Aliases</span>
|
|
@if(count($aliases) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($aliases) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'aliases'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
<button @click="activeTab = 'ipsets'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'ipsets' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-folder-3-line class="size-4" />
|
|
<span>IP Sets</span>
|
|
@if(count($ipsets) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($ipsets) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'ipsets'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
<button @click="activeTab = 'logs'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'logs' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-file-list-3-line class="size-4" />
|
|
<span>Firewall Logs</span>
|
|
@if(count($firewallLogs) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($firewallLogs) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'logs'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
<button @click="activeTab = 'saved'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'saved' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-save-3-line class="size-4" />
|
|
<span>Saved Configurations</span>
|
|
@if(count($savedConfigurations) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($savedConfigurations) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'saved'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
{{-- RULES TAB --}}
|
|
<div x-show="activeTab === 'rules'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">Firewall Rules</h3>
|
|
<button type="button" wire:click="startCreateRule" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
|
|
<x-ri-add-line class="size-4" />
|
|
Add Rule
|
|
</button>
|
|
</div>
|
|
|
|
@if(count($rules) > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead>
|
|
<tr class="border-b border-gray-100 dark:border-gray-800">
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Order</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Pos</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Action</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Direction</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Source</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Destination</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Protocol</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Ports</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Status</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($rules as $index => $rule)
|
|
<tr class="border-b border-gray-50 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
|
<td class="py-3 px-4 text-sm">
|
|
<div class="flex flex-col gap-1">
|
|
@if($index > 0)
|
|
<button type="button"
|
|
wire:click="reorderRules({{ $rule['pos'] }}, {{ $rules[$index - 1]['pos'] }})"
|
|
wire:key="move-up-{{ $index }}"
|
|
class="text-gray-400 hover:text-primary transition-colors p-0.5 hover:bg-primary/10 rounded"
|
|
title="Move up">
|
|
<x-ri-arrow-up-s-line class="size-4" />
|
|
</button>
|
|
@else
|
|
<div class="size-4 p-0.5"></div>
|
|
@endif
|
|
@if($index < count($rules) - 1)
|
|
<button type="button"
|
|
wire:click="reorderRules({{ $rule['pos'] }}, {{ $rules[$index + 1]['pos'] }})"
|
|
wire:key="move-down-{{ $index }}"
|
|
class="text-gray-400 hover:text-primary transition-colors p-0.5 hover:bg-primary/10 rounded"
|
|
title="Move down">
|
|
<x-ri-arrow-down-s-line class="size-4" />
|
|
</button>
|
|
@else
|
|
<div class="size-4 p-0.5"></div>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm font-mono">{{ $rule['pos'] }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded-full {{ $rule['action'] === 'ACCEPT' ? 'bg-success/15 text-success' : ($rule['action'] === 'REJECT' ? 'bg-warning/15 text-warning' : 'bg-error/15 text-error') }}">
|
|
{{ $rule['action'] }}
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['direction'] ?? 'IN' }}</td>
|
|
<td class="py-3 px-4 text-sm font-mono text-xs">{{ $rule['source'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm font-mono text-xs">{{ $rule['dest'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['proto'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm font-mono text-xs">{{ $rule['dport'] ?? $rule['sport'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded-full {{ $rule['enable'] ? 'bg-success/15 text-success' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400' }}">
|
|
@if($rule['enable'])
|
|
<span class="w-1.5 h-1.5 bg-success rounded-full"></span>
|
|
Enabled
|
|
@else
|
|
Disabled
|
|
@endif
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<div class="flex items-center gap-1">
|
|
<button wire:click="toggleRule({{ $rule['pos'] }})" class="text-gray-400 hover:text-primary transition-colors p-1 hover:bg-primary/10 rounded" title="{{ $rule['enable'] ? 'Disable' : 'Enable' }}">
|
|
@if($rule['enable'])
|
|
<x-ri-pause-circle-line class="size-4" />
|
|
@else
|
|
<x-ri-play-circle-line class="size-4" />
|
|
@endif
|
|
</button>
|
|
<button wire:click="startEditRule({{ $rule['pos'] }})" class="text-gray-400 hover:text-primary transition-colors p-1 hover:bg-primary/10 rounded" title="Edit">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
<button wire:click="deleteRule({{ $rule['pos'] }})" class="text-gray-400 hover:text-error transition-colors p-1 hover:bg-error/10 rounded" title="Delete">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-list-check"
|
|
title="No Firewall Rules"
|
|
description="Create your first firewall rule to control network traffic">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.button wire:click="startCreateRule" icon="ri-add-line">
|
|
Add Rule
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- ALIASES TAB --}}
|
|
<div x-show="activeTab === 'aliases'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">IP Aliases</h3>
|
|
<button type="button" wire:click="startCreateAlias" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
|
|
<x-ri-add-line class="size-4" />
|
|
Add Alias
|
|
</button>
|
|
</div>
|
|
|
|
@if(count($aliases) > 0)
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
@foreach($aliases as $alias)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-gray-200 dark:hover:border-gray-600 transition-colors">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="min-w-0 flex-1">
|
|
<h4 class="font-semibold text-sm truncate">{{ $alias['name'] }}</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ $alias['comment'] ?? 'No description' }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-1 ml-2">
|
|
<button wire:click="startEditAlias('{{ $alias['name'] }}')" class="text-gray-400 hover:text-primary transition-colors p-1 hover:bg-primary/10 rounded" title="Edit">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
<button wire:click="deleteAlias('{{ $alias['name'] }}')" class="text-gray-400 hover:text-error transition-colors p-1 hover:bg-error/10 rounded" title="Delete">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2">
|
|
@if($alias['cidr'])
|
|
@foreach(explode(',', $alias['cidr']) as $cidr)
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-global-line class="size-3 text-gray-400" />
|
|
<code class="text-xs font-mono bg-white dark:bg-gray-900 px-2 py-1 rounded">{{ trim($cidr) }}</code>
|
|
</div>
|
|
@endforeach
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-bookmark-line"
|
|
title="No IP Aliases"
|
|
description="Create IP aliases to group addresses for easier rule management">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.button wire:click="startCreateAlias" icon="ri-add-line">
|
|
Add Alias
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- IPSETS TAB --}}
|
|
<div x-show="activeTab === 'ipsets'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">IP Sets</h3>
|
|
<button type="button" wire:click="startCreateIpset" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
|
|
<x-ri-add-line class="size-4" />
|
|
Add IP Set
|
|
</button>
|
|
</div>
|
|
|
|
@if(count($ipsets) > 0)
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
@foreach($ipsets as $ipset)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-gray-200 dark:hover:border-gray-600 transition-colors">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="min-w-0 flex-1">
|
|
<h4 class="font-semibold text-sm">{{ $ipset['name'] }}</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ $ipset['comment'] ?? 'No description' }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-1 ml-2">
|
|
<button wire:click="startEditIpset('{{ $ipset['name'] }}')" class="text-gray-400 hover:text-primary transition-colors p-1 hover:bg-primary/10 rounded" title="Edit">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
<button wire:click="deleteIpset('{{ $ipset['name'] }}')" class="text-gray-400 hover:text-error transition-colors p-1 hover:bg-error/10 rounded" title="Delete">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="text-gray-500 dark:text-gray-400">Addresses:</span>
|
|
<span class="font-mono">{{ count($ipset['cidr'] ?? []) }}</span>
|
|
</div>
|
|
@if(isset($ipset['cidr']) && count($ipset['cidr']) > 0)
|
|
<div class="max-h-32 overflow-y-auto space-y-1">
|
|
@foreach(array_slice($ipset['cidr'], 0, 5) as $cidr)
|
|
<code class="text-xs font-mono bg-white dark:bg-gray-900 px-2 py-1 rounded block">{{ $cidr }}</code>
|
|
@endforeach
|
|
@if(count($ipset['cidr']) > 5)
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">... and {{ count($ipset['cidr']) - 5 }} more</p>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-folder-3-line"
|
|
title="No IP Sets"
|
|
description="Create IP sets to manage collections of addresses for advanced filtering">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.button wire:click="startCreateIpset" icon="ri-add-line">
|
|
Add IP Set
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- LOGS TAB --}}
|
|
<div x-show="activeTab === 'logs'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">Firewall Logs</h3>
|
|
<div class="flex items-center gap-2">
|
|
<x-proxmox::ui.button wire:click="refreshLogs" variant="secondary" size="sm" icon="ri-refresh-line">
|
|
Refresh
|
|
</x-proxmox::ui.button>
|
|
<x-proxmox::ui.button wire:click="clearLogs" variant="danger" size="sm" icon="ri-delete-bin-line">
|
|
Clear Logs
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</div>
|
|
|
|
@if(count($firewallLogs) > 0)
|
|
<div class="space-y-3">
|
|
@foreach($firewallLogs as $log)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700">
|
|
<div class="flex items-start justify-between mb-2">
|
|
<div class="flex items-center gap-2">
|
|
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded-full {{ $log['action'] === 'ACCEPT' ? 'bg-success/15 text-success' : 'bg-error/15 text-error' }}">
|
|
{{ $log['action'] }}
|
|
</span>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ $log['time'] }}</span>
|
|
</div>
|
|
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">{{ $log['interface'] ?? 'N/A' }}</span>
|
|
</div>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">Source</span>
|
|
<div class="font-mono">{{ $log['src'] ?? 'N/A' }}</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">Destination</span>
|
|
<div class="font-mono">{{ $log['dst'] ?? 'N/A' }}</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">Protocol</span>
|
|
<div>{{ $log['proto'] ?? 'N/A' }}</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">Port</span>
|
|
<div class="font-mono">{{ $log['dport'] ?? $log['sport'] ?? 'N/A' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-file-list-3-line"
|
|
title="No Firewall Logs"
|
|
description="Firewall activity will appear here when rules are triggered" />
|
|
@endif
|
|
</div>
|
|
|
|
{{-- SAVED CONFIGURATIONS TAB --}}
|
|
<div x-show="activeTab === 'saved'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">Saved Configurations</h3>
|
|
<x-proxmox::ui.button wire:click="saveCurrentConfiguration" icon="ri-save-3-line">
|
|
Save Current Config
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
|
|
@if(count($savedConfigurations) > 0)
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
@foreach($savedConfigurations as $config)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-gray-200 dark:hover:border-gray-600 transition-colors">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="min-w-0 flex-1">
|
|
<h4 class="font-semibold text-sm truncate">{{ $config['name'] ?? 'Configuration ' . $config['id'] }}</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ $config['description'] ?? 'No description' }}</p>
|
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">{{ $config['created_at'] }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-1 ml-2">
|
|
<button wire:click="loadConfiguration('{{ $config['id'] }}')" class="text-gray-400 hover:text-success transition-colors p-1 hover:bg-success/10 rounded" title="Load">
|
|
<x-ri-download-line class="size-4" />
|
|
</button>
|
|
<button wire:click="deleteConfiguration('{{ $config['id'] }}')" class="text-gray-400 hover:text-error transition-colors p-1 hover:bg-error/10 rounded" title="Delete">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1 text-xs text-gray-500 dark:text-gray-400">
|
|
<div>Rules: {{ $config['rules_count'] ?? 0 }}</div>
|
|
<div>Aliases: {{ $config['aliases_count'] ?? 0 }}</div>
|
|
<div>IP Sets: {{ $config['ipsets_count'] ?? 0 }}</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-save-3-line"
|
|
title="No Saved Configurations"
|
|
description="Save your current firewall configuration for backup or reuse">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.button wire:click="saveCurrentConfiguration" icon="ri-save-3-line">
|
|
Save Current Config
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{{-- Policy Info Modal --}}
|
|
@if($showPolicyInfoModal)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-3xl bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl max-h-[85vh] overflow-y-auto">
|
|
<button wire:click="closePolicyInfoModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="p-6">
|
|
<div class="flex items-center gap-3 mb-6">
|
|
<div class="w-12 h-12 rounded-xl bg-primary flex items-center justify-center">
|
|
<x-ri-information-line class="size-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold">Understanding Firewall Policies</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">How default policies control your network traffic</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-6 prose prose-sm max-w-none">
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-lg">
|
|
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
|
|
<x-ri-arrow-down-line class="size-4 text-warning" />
|
|
Policy In (Incoming Traffic)
|
|
</h4>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">Controls what happens to traffic coming INTO your VM when it doesn't match any explicit firewall rule.</p>
|
|
<ul class="space-y-2 text-sm">
|
|
<li><strong class="text-success">ACCEPT:</strong> Allow all incoming traffic by default (permissive, requires explicit DENY rules)</li>
|
|
<li><strong class="text-warning">REJECT:</strong> Block incoming traffic and send rejection notification to sender</li>
|
|
<li><strong class="text-error">DROP:</strong> Silently discard incoming traffic without notifying sender (recommended)</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-lg">
|
|
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
|
|
<x-ri-arrow-up-line class="size-4 text-info" />
|
|
Policy Out (Outgoing Traffic)
|
|
</h4>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">Controls what happens to traffic going OUT of your VM when it doesn't match any explicit firewall rule.</p>
|
|
<ul class="space-y-2 text-sm">
|
|
<li><strong class="text-success">ACCEPT:</strong> Allow all outgoing traffic by default (recommended for most VMs)</li>
|
|
<li><strong class="text-warning">REJECT:</strong> Block outgoing traffic and notify your VM</li>
|
|
<li><strong class="text-error">DROP:</strong> Silently discard outgoing traffic</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="p-4 bg-warning border border-warning/30 rounded-lg">
|
|
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
|
|
<x-ri-lock-line class="size-4 text-warning" />
|
|
Creating a Whitelist Configuration
|
|
</h4>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">To create a strict whitelist (only allow specific traffic):</p>
|
|
<ol class="list-decimal list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
|
<li>Set <strong>Policy In</strong> to <strong>DROP</strong></li>
|
|
<li>Set <strong>Policy Out</strong> to <strong>DROP</strong> (if restricting outbound)</li>
|
|
<li>Create explicit <strong>ACCEPT</strong> rules for allowed traffic</li>
|
|
<li>Everything not explicitly allowed will be blocked</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div class="p-4 bg-info border border-info/30 rounded-lg">
|
|
<h4 class="font-semibold text-base mb-2">Additional Filters</h4>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2"><strong>IP Filter:</strong> Validates that source/destination IP addresses in packets match the VM's assigned IPs. Helps prevent IP spoofing.</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300"><strong>MAC Filter:</strong> Validates that MAC addresses match the VM's network adapters. Provides additional layer of security.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end mt-6">
|
|
<button wire:click="closePolicyInfoModal" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
Got it
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Rule Form Modal --}}
|
|
@if($showRuleForm)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
|
|
x-data="{
|
|
macroDefinitions: @js($this->macroDefinitions),
|
|
currentMacro: @entangle('ruleMacro'),
|
|
handleMacroChange() {
|
|
if (this.currentMacro && this.currentMacro !== '' && this.macroDefinitions[this.currentMacro]) {
|
|
const macroDef = this.macroDefinitions[this.currentMacro];
|
|
if (macroDef.proto) {
|
|
$wire.set('ruleProto', macroDef.proto);
|
|
}
|
|
if (macroDef.dport) {
|
|
$wire.set('ruleDport', macroDef.dport);
|
|
}
|
|
}
|
|
}
|
|
}"
|
|
x-init="$watch('currentMacro', () => handleMacroChange())">
|
|
<div class="relative w-full max-w-4xl bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl max-h-[90vh] overflow-y-auto">
|
|
<button wire:click="cancelRuleForm" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors z-10">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="p-6">
|
|
<h3 class="text-xl font-semibold mb-6">{{ $editingRuleIndex !== null ? 'Edit' : 'Create' }} Firewall Rule</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{{-- Action --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Action</label>
|
|
<select wire:model="ruleAction" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
<option value="ACCEPT">Accept</option>
|
|
<option value="REJECT">Reject</option>
|
|
<option value="DROP">Drop</option>
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Type --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Direction</label>
|
|
<select wire:model="ruleType" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
<option value="in">Incoming</option>
|
|
<option value="out">Outgoing</option>
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Macro --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Macro (Pre-defined Service)</label>
|
|
<select wire:model="ruleMacro" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
@foreach($availableMacros as $value => $label)
|
|
<option value="{{ $value }}">{{ $label }}</option>
|
|
@endforeach
|
|
</select>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
<x-ri-lightbulb-line class="size-3 inline mr-0.5" />
|
|
Selecting a macro will automatically set the protocol and port(s)
|
|
</p>
|
|
</div>
|
|
|
|
{{-- Interface --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Network Interface</label>
|
|
<select wire:model="ruleIface" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
<option value="">Any</option>
|
|
@foreach($networkAdapters as $adapter)
|
|
<option value="{{ $adapter['name'] }}">{{ $adapter['name'] }} ({{ ucfirst($adapter['model'] ?? 'Unknown') }})</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Source --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Source IP/CIDR</label>
|
|
<input type="text" wire:model="ruleSource" placeholder="e.g., 192.168.1.0/24 or leave empty for any" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Can use alias names or leave empty for any source</p>
|
|
</div>
|
|
|
|
{{-- Destination --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Destination IP/CIDR</label>
|
|
<select wire:model="ruleDest" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
<option value="">Any</option>
|
|
@foreach($availableIps as $ipData)
|
|
<option value="{{ $ipData['ip'] }}">{{ $ipData['ip'] }} ({{ $ipData['label'] }})</option>
|
|
@endforeach
|
|
@foreach($aliases as $alias)
|
|
<option value="{{ $alias['name'] }}">{{ $alias['name'] }} (Alias)</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Protocol --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">
|
|
Protocol
|
|
<span class="text-xs font-normal text-gray-500">(auto-filled by macro)</span>
|
|
</label>
|
|
<select wire:model="ruleProto" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
<option value="">Any</option>
|
|
<option value="tcp">TCP</option>
|
|
<option value="udp">UDP</option>
|
|
<option value="icmp">ICMP</option>
|
|
<option value="igmp">IGMP</option>
|
|
<option value="ah">AH (IPsec Authentication Header)</option>
|
|
<option value="esp">ESP (IPsec Encapsulating Security Payload)</option>
|
|
<option value="ospf">OSPF</option>
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Destination Port --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">
|
|
Destination Port
|
|
<span class="text-xs font-normal text-gray-500">(auto-filled by macro)</span>
|
|
</label>
|
|
<input type="text" wire:model="ruleDport" placeholder="e.g., 80, 443, 1000:2000" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
</div>
|
|
|
|
{{-- Source Port --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Source Port (Optional)</label>
|
|
<input type="text" wire:model="ruleSport" placeholder="e.g., 80, 443" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
</div>
|
|
|
|
{{-- Log Level --}}
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Logging</label>
|
|
<select wire:model="ruleLog" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
<option value="nolog">No Logging</option>
|
|
<option value="emerg">Emergency</option>
|
|
<option value="alert">Alert</option>
|
|
<option value="crit">Critical</option>
|
|
<option value="err">Error</option>
|
|
<option value="warning">Warning</option>
|
|
<option value="notice">Notice</option>
|
|
<option value="info">Info</option>
|
|
<option value="debug">Debug</option>
|
|
</select>
|
|
</div>
|
|
|
|
{{-- Comment --}}
|
|
<div class="md:col-span-2">
|
|
<label class="block text-sm font-medium mb-2">Comment</label>
|
|
<input type="text" wire:model="ruleComment" placeholder="Optional description" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
</div>
|
|
|
|
{{-- Enable --}}
|
|
<div class="md:col-span-2 flex items-center gap-2">
|
|
<input type="checkbox" wire:model="ruleEnable" id="ruleEnable" class="rounded border-gray-300 dark:border-gray-600">
|
|
<label for="ruleEnable" class="text-sm font-medium">Enable this rule immediately</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button type="button" wire:click="cancelRuleForm" class="px-4 py-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium">
|
|
Cancel
|
|
</button>
|
|
<button type="button" wire:click="saveRule" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
<span wire:loading.remove wire:target="saveRule">Save Rule</span>
|
|
<span wire:loading wire:target="saveRule">Saving...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Delete Rule Modal --}}
|
|
@if($showDeleteRuleModal && $deletingRuleIndex !== null && isset($rules[$deletingRuleIndex]))
|
|
@php $ruleToDelete = $rules[$deletingRuleIndex]; @endphp
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button wire:click="cancelDeleteRule" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
|
|
<x-ri-delete-bin-fill class="size-6 text-error" />
|
|
</div>
|
|
<div class="flex-1">
|
|
<h3 class="text-xl font-bold mb-2">Delete Firewall Rule</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Are you sure you want to delete this rule? This action cannot be undone.</p>
|
|
<div class="p-3 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-lg text-sm space-y-1">
|
|
<div><span class="text-gray-500">Action:</span> <span class="font-semibold">{{ $ruleToDelete['action'] ?? '-' }}</span></div>
|
|
<div><span class="text-gray-500">Source:</span> <span class="font-mono text-xs">{{ $ruleToDelete['source'] ?? 'Any' }}</span></div>
|
|
<div><span class="text-gray-500">Destination:</span> <span class="font-mono text-xs">{{ $ruleToDelete['dest'] ?? 'Any' }}</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3 justify-end">
|
|
<x-proxmox::ui.button wire:click="cancelDeleteRule" variant="secondary">
|
|
Cancel
|
|
</x-proxmox::ui.button>
|
|
<x-proxmox::ui.button wire:click="deleteRule" wire:loading.attr="disabled" variant="danger">
|
|
<span wire:loading.remove wire:target="deleteRule">Delete Rule</span>
|
|
<span wire:loading wire:target="deleteRule">Deleting...</span>
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Alias Form Modal --}}
|
|
@if($showAliasForm)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button wire:click="cancelAliasForm" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<h3 class="text-xl font-semibold mb-6">{{ $editingAliasName ? 'Edit' : 'Create' }} IP Alias</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Alias Name</label>
|
|
<input type="text" wire:model="aliasName" placeholder="e.g., web-servers" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm" {{ $editingAliasName ? 'readonly' : '' }}>
|
|
@error('aliasName') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">IP Address / CIDR</label>
|
|
<input type="text" wire:model="aliasCidr" placeholder="e.g., 192.168.1.100 or 10.0.0.0/24" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
@error('aliasCidr') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Comment (Optional)</label>
|
|
<input type="text" wire:model="aliasComment" placeholder="Description of this alias" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button wire:click="cancelAliasForm" class="px-4 py-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="saveAlias" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
<span wire:loading.remove wire:target="saveAlias">Save Alias</span>
|
|
<span wire:loading wire:target="saveAlias">Saving...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Delete Alias Modal --}}
|
|
@if($showDeleteAliasModal && $deletingAliasName)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-md bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button wire:click="cancelDeleteAlias" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
|
|
<x-ri-delete-bin-fill class="size-6 text-error" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold mb-2">Delete IP Alias</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Delete alias "<span class="font-semibold">{{ $deletingAliasName }}</span>"?</p>
|
|
<p class="text-xs text-gray-500">This may affect firewall rules using this alias.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3 justify-end">
|
|
<x-proxmox::ui.button wire:click="cancelDeleteAlias" variant="secondary">
|
|
Cancel
|
|
</x-proxmox::ui.button>
|
|
<x-proxmox::ui.button wire:click="deleteAlias" wire:loading.attr="disabled" variant="danger">
|
|
<span wire:loading.remove wire:target="deleteAlias">Delete</span>
|
|
<span wire:loading wire:target="deleteAlias">Deleting...</span>
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- IPSet Form Modal --}}
|
|
@if($showIPSetForm)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button wire:click="cancelIPSetForm" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<h3 class="text-xl font-semibold mb-6">Create IP Set</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">IPSet Name</label>
|
|
<input type="text" wire:model="ipsetName" placeholder="e.g., trusted-networks" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
@error('ipsetName') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Comment (Optional)</label>
|
|
<input type="text" wire:model="ipsetComment" placeholder="Description of this IPSet" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
</div>
|
|
|
|
<div class="p-3 bg-info/10 border border-info/30 rounded-lg">
|
|
<p class="text-xs text-info">After creating the IPSet, you can add IP addresses and CIDR ranges to it.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button wire:click="cancelIPSetForm" class="px-4 py-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="saveIPSet" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
<span wire:loading.remove wire:target="saveIPSet">Create IPSet</span>
|
|
<span wire:loading wire:target="saveIPSet">Creating...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Delete IPSet Modal --}}
|
|
@if($showDeleteIPSetModal && $deletingIPSetName)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-md bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button wire:click="cancelDeleteIPSet" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
|
|
<x-ri-delete-bin-fill class="size-6 text-error" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold mb-2">Delete IP Set</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Delete IPSet "<span class="font-semibold">{{ $deletingIPSetName }}</span>"?</p>
|
|
<p class="text-xs text-gray-500">All members will be removed and rules using this IPSet may be affected.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3 justify-end">
|
|
<x-proxmox::ui.button wire:click="cancelDeleteIPSet" variant="secondary">
|
|
Cancel
|
|
</x-proxmox::ui.button>
|
|
<x-proxmox::ui.button wire:click="deleteIPSet" wire:loading.attr="disabled" variant="danger">
|
|
<span wire:loading.remove wire:target="deleteIPSet">Delete</span>
|
|
<span wire:loading wire:target="deleteIPSet">Deleting...</span>
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- IPSet Member Form Modal --}}
|
|
@if($showIPSetMemberForm && $selectedIPSetName)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button wire:click="cancelIPSetMemberForm" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<h3 class="text-xl font-semibold mb-6">Add IP/CIDR to {{ $selectedIPSetName }}</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">IP Address / CIDR</label>
|
|
<input type="text" wire:model="memberCidr" placeholder="e.g., 192.168.1.100 or 10.0.0.0/16" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
@error('memberCidr') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Comment (Optional)</label>
|
|
<input type="text" wire:model="memberComment" placeholder="Description" class="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-sm">
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<input type="checkbox" wire:model="memberNomatch" id="memberNomatch" class="rounded border-gray-300 dark:border-gray-600">
|
|
<label for="memberNomatch" class="text-sm">
|
|
<span class="font-medium">No Match</span>
|
|
<span class="text-gray-500">(Exclude this IP from the set)</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button wire:click="cancelIPSetMemberForm" class="px-4 py-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="saveIPSetMember" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
<span wire:loading.remove wire:target="saveIPSetMember">Add Member</span>
|
|
<span wire:loading wire:target="saveIPSetMember">Adding...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Delete IPSet Member Modal --}}
|
|
@if($showDeleteIPSetMemberModal && $deletingMemberCidr)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-md bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button wire:click="cancelDeleteIPSetMember" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
|
|
<x-ri-close-circle-fill class="size-6 text-error" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold mb-2">Remove Member</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2">Remove "<span class="font-mono font-semibold">{{ $deletingMemberCidr }}</span>" from {{ $selectedIPSetName }}?</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-3 justify-end">
|
|
<x-proxmox::ui.button wire:click="cancelDeleteIPSetMember" variant="secondary">
|
|
Cancel
|
|
</x-proxmox::ui.button>
|
|
<x-proxmox::ui.button wire:click="deleteIPSetMember" wire:loading.attr="disabled" variant="danger">
|
|
<span wire:loading.remove wire:target="deleteIPSetMember">Remove</span>
|
|
<span wire:loading wire:target="deleteIPSetMember">Removing...</span>
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- Save Configuration Modal --}}
|
|
@if($showSaveConfigModal)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-lg bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl p-6">
|
|
<button wire:click="closeSaveConfigModal" class="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-start gap-4 mb-6">
|
|
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
|
<x-ri-save-3-line class="size-6 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold mb-1">Save Current Configuration</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">Save your current firewall rules and policies as a reusable configuration</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Configuration Name</label>
|
|
<input type="text" wire:model="newConfigName" placeholder="e.g., Web Server Rules"
|
|
class="w-full px-4 py-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:border-primary focus:outline-none transition-colors">
|
|
@error('newConfigName')
|
|
<p class="text-error text-xs mt-1">{{ $message }}</p>
|
|
@enderror
|
|
</div>
|
|
|
|
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-lg p-4">
|
|
<h4 class="text-sm font-semibold mb-3">Configuration Summary</h4>
|
|
<div class="grid grid-cols-2 gap-3 text-sm">
|
|
<div class="flex items-center justify-between p-2 bg-white dark:bg-gray-900 rounded">
|
|
<span class="text-gray-500">Rules</span>
|
|
<span class="font-semibold">{{ count($rules) }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between p-2 bg-white dark:bg-gray-900 rounded">
|
|
<span class="text-gray-500">Incoming Policy</span>
|
|
<span class="font-semibold {{ $policyIn === 'DROP' ? 'text-error' : 'text-success' }}">{{ $policyIn }}</span>
|
|
</div>
|
|
<div class="flex items-center justify-between p-2 bg-white dark:bg-gray-900 rounded">
|
|
<span class="text-gray-500">Outgoing Policy</span>
|
|
<span class="font-semibold {{ $policyOut === 'DROP' ? 'text-error' : 'text-success' }}">{{ $policyOut }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<button wire:click="closeSaveConfigModal" class="px-4 py-2 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="saveConfiguration" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
<span wire:loading.remove wire:target="saveConfiguration">Save Configuration</span>
|
|
<span wire:loading wire:target="saveConfiguration">Saving...</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
</div>
|
|
|
|
@endif
|
|
</x-proxmox::layout> |