1406 lines
97 KiB
PHP
1406 lines
97 KiB
PHP
<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.4;
|
|
cursor: pointer;
|
|
}
|
|
.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.4;
|
|
cursor: pointer;
|
|
}
|
|
</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;">
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between group mb-6">
|
|
<div class="flex items-start" style="gap: 15px;">
|
|
<div style="width: 2.75rem; height: 2.75rem; display: flex; align-items: center; justify-content: center; flex-shrink: 0; border-radius: .5rem; background: rgba(13, 110, 253, 0.1);" class="transition-transform duration-300 group-hover:scale-105">
|
|
<x-ri-shield-line class="size-5 transition-colors duration-200" style="color: #0d6efd;" />
|
|
</div>
|
|
<div style="display: flex; flex-direction: column; gap: .25rem;">
|
|
<div class="flex items-center gap-2">
|
|
<h2 style="font-size: 1.125rem; font-weight: 600; color: #212529; margin: 0;">Firewall Management</h2>
|
|
<div class="relative" x-data="{ open: false }" x-cloak>
|
|
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
|
|
<x-ri-question-line class="size-4" />
|
|
</button>
|
|
<!-- Modal Backdrop -->
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0"
|
|
x-transition:enter-end="opacity-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100"
|
|
x-transition:leave-end="opacity-0"
|
|
@click="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm"
|
|
style="display: none;">
|
|
</div>
|
|
<!-- Modal -->
|
|
<div x-show="open"
|
|
x-transition:enter="transition ease-out duration-300"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-200"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
@click.away="open = false"
|
|
@keydown.escape.window="open = false"
|
|
class="fixed inset-0 z-[60] flex items-center justify-center p-4"
|
|
style="display: none;">
|
|
<div class="relative w-full max-w-2xl max-h-[90vh] overflow-y-auto bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
|
|
<button @click="open = false" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="flex items-center gap-3 mb-4 pr-8">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-shield-line class="size-5 text-primary" />
|
|
</div>
|
|
<h6 class="font-sans text-lg font-semibold leading-tight">
|
|
Firewall Management
|
|
</h6>
|
|
</div>
|
|
<p class="block font-sans text-sm font-normal leading-relaxed 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. Configure granular rules to allow or block specific traffic patterns, manage default policies for incoming and outgoing traffic, and organize IP addresses into reusable aliases and sets.
|
|
</p>
|
|
<div class="pt-4 mt-4 border-t border-neutral/30 space-y-2.5">
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Firewall Rules:</span> Create granular rules to allow or block specific traffic patterns based on source/destination IP addresses, protocols, ports, and network interfaces. Rules are processed in order with the first matching rule determining the action.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Default Policies:</span> Set default actions (ACCEPT, REJECT, or DROP) for incoming and outgoing traffic that doesn't match any specific rule. Use DROP for incoming traffic to create a secure whitelist configuration.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">IP Aliases:</span> Define named groups of IP addresses or CIDR ranges for easier rule management. Use aliases to create reusable address definitions that can be referenced in multiple rules.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">IP Sets:</span> Manage collections of IP addresses for advanced filtering scenarios. IP Sets allow you to create groups of addresses that can be efficiently matched against traffic.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Firewall Logs:</span> Monitor firewall activity and view logged events when rules with logging enabled are triggered. Logs help troubleshoot connectivity issues and identify security threats.
|
|
</p>
|
|
<p class="block font-sans text-xs font-normal leading-relaxed text-base/75">
|
|
<span class="font-semibold text-base/90">Network Adapters:</span> Control firewall state independently for each network adapter attached to your VM. This allows granular control over which interfaces have firewall protection enabled.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p style="font-size: .875rem; color: #6c757d; margin: 0;">Configure firewall rules, policies, aliases, and IPSets for your virtual machine.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Error Modal --}}
|
|
@if($error && $showErrorModal)
|
|
<x-proxmox::ui.modal name="firewall-error" 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>
|
|
</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>
|
|
|
|
<button type="button" wire:click="updatePolicies" wire:loading.attr="disabled" class="w-full mt-4 px-4 py-2 bg-success text-white rounded-lg hover:bg-success/90 transition-colors text-sm font-medium flex items-center justify-center gap-2">
|
|
<span wire:loading.remove wire:target="updatePolicies">Update Policies</span>
|
|
<span wire:loading wire:target="updatePolicies">Updating...</span>
|
|
</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>
|
|
</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" style="gap: 15px;">
|
|
<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" style="gap: 15px;">
|
|
<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" style="gap: 15px;">
|
|
<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" style="gap: 15px;">
|
|
<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" style="gap: 15px;">
|
|
<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({{ $index }})" 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({{ $index }})" 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="confirmDeleteRule({{ $index }})" 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">
|
|
<button type="button" wire:click="refreshLogs" class="px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 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 flex items-center gap-2 whitespace-nowrap">
|
|
<x-ri-refresh-line class="size-4" />
|
|
Refresh
|
|
</button>
|
|
<button type="button" wire:click="clearLogs" class="px-4 py-2 bg-error text-white rounded-lg hover:bg-error/90 transition-colors text-sm font-medium flex items-center gap-2 whitespace-nowrap">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
Clear Logs
|
|
</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>
|
|
<button type="button" wire:click="saveCurrentConfiguration" 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 whitespace-nowrap">
|
|
<x-ri-save-3-line class="size-4" />
|
|
Save Current Config
|
|
</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="confirmApplyConfiguration({{ $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="confirmDeleteConfiguration({{ $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">
|
|
<button type="button" wire:click="saveCurrentConfiguration" 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 whitespace-nowrap">
|
|
<x-ri-save-3-line class="size-4" />
|
|
Save Current Config
|
|
</button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</section>
|
|
@endif
|
|
|
|
{{-- 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="tcp" selected>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">
|
|
<button wire:click="cancelDeleteRule" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors text-sm font-medium">
|
|
Cancel
|
|
</button>
|
|
<button wire:click="deleteRule" wire:loading.attr="disabled" class="px-4 py-2 rounded-lg text-sm font-medium text-white transition-colors" style="background-color: #dc2626;" onmouseover="this.style.backgroundColor='#b91c1c'" onmouseout="this.style.backgroundColor='#dc2626'">
|
|
<span wire:loading.remove wire:target="deleteRule">Delete Rule</span>
|
|
<span wire:loading wire:target="deleteRule">Deleting...</span>
|
|
</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> |