feat: Remove captive portal and add auto-zoning to Client Guardian (v0.6.0-r24)
Major enhancements to Client Guardian: **Removed Captive Portal:** - Deleted portal.js and captive.js views - Removed portal configuration from UCI - Removed portal RPC methods (get_portal, update_portal, list_sessions, authorize_client, deauthorize_client) - Cleaned menu and ACL definitions - Updated default policy from 'captive' to 'quarantine' **Added Auto-Zoning System:** - Implemented get_vendor_from_mac() for OUI lookups - Added apply_auto_zoning() with rule-based zone assignment - Support for vendor, hostname pattern, and MAC prefix matching - 8 pre-configured auto-zoning rules (IoT devices, mobile, guests) - Auto-parking zone for unmatched clients - GridSection UI for managing auto-zoning rules **Threat Intelligence Integration:** - Added threat_policy UCI section - Auto-ban/quarantine based on threat score thresholds - Threat indicators on client displays - Integration with Security Threats Dashboard **Dashboard Improvements:** - Fixed boolean conversion (UCI "true"/"false" to JSON 0/1) - Fixed RPC expect parameter issues causing empty arrays - Added real-time polling with configurable intervals - Removed all window.location.reload() calls - Smooth DOM updates without page flickers **Settings Enhancements:** - Added reactiveness section (auto-refresh toggle, interval) - Added threat intelligence settings - Removed captive portal settings section - Updated policy descriptions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1823913582
commit
0564de0811
@ -41,11 +41,6 @@ var callParental = rpc.declare({
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callPortal = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'portal',
|
||||
expect: { }
|
||||
});
|
||||
|
||||
var callAlerts = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
@ -95,13 +90,6 @@ var callUpdateZone = rpc.declare({
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
var callUpdatePortal = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'update_portal',
|
||||
params: ['title', 'subtitle', 'accent_color'],
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
var callSendTestAlert = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'send_test_alert',
|
||||
@ -109,13 +97,6 @@ var callSendTestAlert = rpc.declare({
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
// Nodogsplash Captive Portal Methods
|
||||
var callListSessions = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'list_sessions',
|
||||
expect: { sessions: [] }
|
||||
});
|
||||
|
||||
var callGetPolicy = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'get_policy',
|
||||
@ -125,21 +106,13 @@ var callGetPolicy = rpc.declare({
|
||||
var callSetPolicy = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'set_policy',
|
||||
params: ['policy', 'portal_enabled', 'auto_approve', 'session_timeout'],
|
||||
params: ['policy', 'auto_approve', 'session_timeout'],
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
var callAuthorizeClient = rpc.declare({
|
||||
var callSyncZones = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'authorize_client',
|
||||
params: ['mac', 'ip'],
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
var callDeauthorizeClient = rpc.declare({
|
||||
object: 'luci.client-guardian',
|
||||
method: 'deauthorize_client',
|
||||
params: ['mac', 'ip'],
|
||||
method: 'sync_zones',
|
||||
expect: { success: false }
|
||||
});
|
||||
|
||||
@ -165,6 +138,42 @@ function formatBytes(bytes) {
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function getDeviceIcon(hostname, mac) {
|
||||
hostname = (hostname || '').toLowerCase();
|
||||
mac = (mac || '').toLowerCase();
|
||||
|
||||
// Mobile devices
|
||||
if (hostname.match(/android|iphone|ipad|mobile|phone|samsung|xiaomi|huawei/))
|
||||
return '📱';
|
||||
|
||||
// Computers
|
||||
if (hostname.match(/pc|laptop|desktop|macbook|imac|windows|linux|ubuntu/))
|
||||
return '💻';
|
||||
|
||||
// IoT devices
|
||||
if (hostname.match(/camera|bulb|switch|sensor|thermostat|doorbell|lock/))
|
||||
return '📷';
|
||||
|
||||
// Smart TV / Media
|
||||
if (hostname.match(/tv|roku|chromecast|firestick|appletv|media/))
|
||||
return '📺';
|
||||
|
||||
// Gaming
|
||||
if (hostname.match(/playstation|xbox|nintendo|switch|steam/))
|
||||
return '🎮';
|
||||
|
||||
// Network equipment
|
||||
if (hostname.match(/router|switch|ap|access[-_]?point|bridge/))
|
||||
return '🌐';
|
||||
|
||||
// Printers
|
||||
if (hostname.match(/printer|print|hp-|canon-|epson-/))
|
||||
return '🖨️';
|
||||
|
||||
// Default
|
||||
return '🔌';
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
// Core methods
|
||||
getStatus: callStatus,
|
||||
@ -172,7 +181,6 @@ return baseclass.extend({
|
||||
getClient: callGetClient,
|
||||
getZones: callZones,
|
||||
getParental: callParental,
|
||||
getPortal: callPortal,
|
||||
getAlerts: callAlerts,
|
||||
getLogs: callLogs,
|
||||
|
||||
@ -184,18 +192,14 @@ return baseclass.extend({
|
||||
|
||||
// Configuration
|
||||
updateZone: callUpdateZone,
|
||||
updatePortal: callUpdatePortal,
|
||||
sendTestAlert: callSendTestAlert,
|
||||
|
||||
// Nodogsplash Captive Portal
|
||||
listSessions: callListSessions,
|
||||
syncZones: callSyncZones,
|
||||
getPolicy: callGetPolicy,
|
||||
setPolicy: callSetPolicy,
|
||||
authorizeClient: callAuthorizeClient,
|
||||
deauthorizeClient: callDeauthorizeClient,
|
||||
|
||||
// Utility functions
|
||||
formatMac: formatMac,
|
||||
formatDuration: formatDuration,
|
||||
formatBytes: formatBytes
|
||||
formatBytes: formatBytes,
|
||||
getDeviceIcon: getDeviceIcon
|
||||
});
|
||||
|
||||
@ -9,41 +9,50 @@
|
||||
--cg-bg-tertiary: #251a1a;
|
||||
--cg-border: #3d2828;
|
||||
--cg-border-light: #4a3333;
|
||||
|
||||
|
||||
--cg-text-primary: #fafafa;
|
||||
--cg-text-secondary: #b8a8a8;
|
||||
--cg-text-muted: #8a7575;
|
||||
|
||||
--cg-accent-red: #ef4444;
|
||||
|
||||
/* SecuBox Brand Colors (Indigo/Purple) */
|
||||
--cg-primary: #6366f1;
|
||||
--cg-primary-end: #8b5cf6;
|
||||
|
||||
/* Accent Colors */
|
||||
--cg-accent-orange: #f97316;
|
||||
--cg-accent-amber: #f59e0b;
|
||||
--cg-accent-green: #22c55e;
|
||||
--cg-accent-blue: #3b82f6;
|
||||
--cg-accent-purple: #8b5cf6;
|
||||
--cg-accent-cyan: #06b6d4;
|
||||
|
||||
--cg-danger: #dc2626;
|
||||
|
||||
/* State Colors */
|
||||
--cg-danger: #ef4444;
|
||||
--cg-warning: #f59e0b;
|
||||
--cg-success: #16a34a;
|
||||
--cg-info: #0284c7;
|
||||
|
||||
--cg-gradient: linear-gradient(135deg, #ef4444, #dc2626, #b91c1c);
|
||||
--cg-gradient-soft: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(220, 38, 38, 0.1));
|
||||
|
||||
|
||||
/* Primary Gradients (SecuBox Indigo/Purple) */
|
||||
--cg-gradient: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
--cg-gradient-soft: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.1));
|
||||
|
||||
/* Zone Colors (Keep as-is) */
|
||||
--cg-zone-private: #22c55e;
|
||||
--cg-zone-iot: #f59e0b;
|
||||
--cg-zone-kids: #06b6d4;
|
||||
--cg-zone-guest: #8b5cf6;
|
||||
--cg-zone-quarantine: #ef4444;
|
||||
--cg-zone-blocked: #6b7280;
|
||||
|
||||
|
||||
/* Typography */
|
||||
--cg-font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
--cg-font-sans: 'Inter', -apple-system, sans-serif;
|
||||
|
||||
|
||||
/* Layout */
|
||||
--cg-radius: 8px;
|
||||
--cg-radius-lg: 12px;
|
||||
--cg-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
||||
--cg-shadow-glow: 0 0 30px rgba(239, 68, 68, 0.3);
|
||||
--cg-shadow-glow: 0 0 30px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
/* Base */
|
||||
@ -164,14 +173,14 @@
|
||||
}
|
||||
|
||||
.cg-status-badge.approved {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: var(--cg-accent-green);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
color: var(--cg-primary);
|
||||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.cg-status-badge.quarantine {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: var(--cg-accent-red);
|
||||
color: var(--cg-danger);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
@ -259,8 +268,8 @@
|
||||
}
|
||||
|
||||
.cg-client-item:hover {
|
||||
border-color: var(--cg-accent-red);
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
border-color: var(--cg-primary);
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.cg-client-item.online {
|
||||
@ -273,7 +282,7 @@
|
||||
}
|
||||
|
||||
.cg-client-item.quarantine {
|
||||
border-left: 3px solid var(--cg-accent-red);
|
||||
border-left: 3px solid var(--cg-danger);
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
|
||||
@ -373,13 +382,37 @@
|
||||
|
||||
.cg-client-action:hover {
|
||||
background: var(--cg-bg-tertiary);
|
||||
border-color: var(--cg-accent-red);
|
||||
border-color: var(--cg-primary);
|
||||
}
|
||||
|
||||
.cg-client-action.approve:hover { border-color: var(--cg-accent-green); background: rgba(34, 197, 94, 0.1); }
|
||||
.cg-client-action.ban:hover { border-color: var(--cg-danger); background: rgba(220, 38, 38, 0.1); }
|
||||
.cg-client-action.edit:hover { border-color: var(--cg-accent-blue); background: rgba(59, 130, 246, 0.1); }
|
||||
|
||||
/* Threat Indicators */
|
||||
.cg-threat-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: pulse-threat 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-threat {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.cg-client-item .cg-threat-badge:hover {
|
||||
animation: none;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
/* Zone Cards */
|
||||
.cg-zones-grid {
|
||||
display: grid;
|
||||
@ -515,7 +548,7 @@
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.cg-toggle-switch.active { background: var(--cg-accent-red); }
|
||||
.cg-toggle-switch.active { background: var(--cg-primary); }
|
||||
|
||||
.cg-toggle-switch::after {
|
||||
content: '';
|
||||
@ -554,7 +587,7 @@
|
||||
.cg-select:focus,
|
||||
.cg-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--cg-accent-red);
|
||||
border-color: var(--cg-primary);
|
||||
}
|
||||
|
||||
.cg-textarea { min-height: 100px; resize: vertical; }
|
||||
@ -584,7 +617,7 @@
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.cg-btn:hover { border-color: var(--cg-accent-red); }
|
||||
.cg-btn:hover { border-color: var(--cg-primary); }
|
||||
|
||||
.cg-btn-primary {
|
||||
background: var(--cg-gradient);
|
||||
@ -618,7 +651,7 @@
|
||||
padding: 16px;
|
||||
background: var(--cg-bg-tertiary);
|
||||
border-radius: var(--cg-radius);
|
||||
border-left: 4px solid var(--cg-accent-red);
|
||||
border-left: 4px solid var(--cg-danger);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@ -647,8 +680,8 @@
|
||||
|
||||
.cg-schedule-day.active {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: var(--cg-accent-red);
|
||||
color: var(--cg-accent-red);
|
||||
border-color: var(--cg-danger);
|
||||
color: var(--cg-danger);
|
||||
}
|
||||
|
||||
.cg-schedule-day-name { font-size: 11px; font-weight: 600; }
|
||||
@ -729,7 +762,7 @@
|
||||
|
||||
.cg-log-level.info { background: rgba(59, 130, 246, 0.15); color: var(--cg-accent-blue); }
|
||||
.cg-log-level.warning { background: rgba(245, 158, 11, 0.15); color: var(--cg-accent-amber); }
|
||||
.cg-log-level.error { background: rgba(239, 68, 68, 0.15); color: var(--cg-accent-red); }
|
||||
.cg-log-level.error { background: rgba(239, 68, 68, 0.15); color: var(--cg-danger); }
|
||||
|
||||
.cg-log-message { flex: 1; color: var(--cg-text-secondary); }
|
||||
|
||||
@ -761,10 +794,428 @@
|
||||
.client-guardian-dashboard ::-webkit-scrollbar-thumb { background: var(--cg-border); border-radius: 4px; }
|
||||
.client-guardian-dashboard ::-webkit-scrollbar-thumb:hover { background: var(--cg-text-muted); }
|
||||
|
||||
/* Loading States */
|
||||
.cg-status-badge.loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.cg-status-badge.loading .cg-status-dot {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Smooth Transitions */
|
||||
.cg-stat-value {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cg-client-item {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cg-stat-card.updated {
|
||||
animation: flash 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; background: rgba(99, 102, 241, 0.1); }
|
||||
}
|
||||
|
||||
@keyframes client-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }
|
||||
50% { box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); }
|
||||
}
|
||||
|
||||
.cg-client-item.quarantine { animation: client-pulse 2s infinite; }
|
||||
|
||||
/* Wizard Styles */
|
||||
.cg-wizard {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.cg-wizard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.cg-wizard-icon {
|
||||
font-size: 72px;
|
||||
margin-bottom: 16px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.cg-wizard-title {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--cg-text-primary);
|
||||
margin: 0 0 8px 0;
|
||||
background: var(--cg-gradient);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.cg-wizard-subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--cg-text-secondary);
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cg-profiles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.cg-profile-card {
|
||||
background: var(--cg-bg-secondary);
|
||||
border: 2px solid var(--cg-border);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cg-profile-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--cg-gradient);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.cg-profile-card:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cg-profile-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--cg-primary);
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.15);
|
||||
}
|
||||
|
||||
.cg-profile-icon {
|
||||
font-size: 48px;
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cg-profile-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--cg-text-primary);
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cg-profile-desc {
|
||||
font-size: 14px;
|
||||
color: var(--cg-text-secondary);
|
||||
text-align: center;
|
||||
margin-bottom: 16px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.cg-profile-zones {
|
||||
background: var(--cg-bg-tertiary);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cg-profile-zones strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: var(--cg-text-muted);
|
||||
}
|
||||
|
||||
.cg-profile-zone-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.cg-profile-zone-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cg-profile-more {
|
||||
font-size: 12px;
|
||||
color: var(--cg-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.cg-profile-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cg-wizard-footer {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.cg-wizard-note {
|
||||
color: var(--cg-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cg-modal-profile {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.cg-modal-profile h3 {
|
||||
text-align: center;
|
||||
color: var(--cg-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cg-modal-profile p {
|
||||
text-align: center;
|
||||
color: var(--cg-text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cg-modal-profile ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cg-modal-profile li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--cg-border);
|
||||
}
|
||||
|
||||
.cg-modal-profile li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 4px solid rgba(99, 102, 241, 0.1);
|
||||
border-top: 4px solid var(--cg-primary);
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Debug Interface Styles */
|
||||
.cg-debug-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cg-btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cg-debug-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.cg-debug-status-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cg-debug-status-label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: var(--cg-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cg-debug-status-value {
|
||||
font-size: 14px;
|
||||
color: var(--cg-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cg-input-sm {
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cg-system-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.cg-info-item {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--cg-border);
|
||||
}
|
||||
|
||||
.cg-info-label {
|
||||
font-weight: 600;
|
||||
color: var(--cg-text-secondary);
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.cg-info-value {
|
||||
color: var(--cg-text-primary);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cg-log-container {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--cg-border);
|
||||
border-radius: 8px;
|
||||
background: var(--cg-bg-tertiary);
|
||||
}
|
||||
|
||||
.cg-log-entry {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--cg-border);
|
||||
font-family: var(--cg-font-mono);
|
||||
font-size: 13px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.cg-log-entry:hover {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.cg-log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.cg-log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.cg-log-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.cg-log-level {
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.cg-log-error .cg-log-level {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.cg-log-warn .cg-log-level {
|
||||
background: rgba(245, 158, 11, 0.15);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.cg-log-info .cg-log-level {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.cg-log-debug .cg-log-level {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.cg-log-trace .cg-log-level {
|
||||
background: rgba(107, 114, 128, 0.15);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.cg-log-time {
|
||||
color: var(--cg-text-muted);
|
||||
font-size: 11px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.cg-log-message {
|
||||
color: var(--cg-text-primary);
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cg-log-details {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.cg-log-details summary {
|
||||
cursor: pointer;
|
||||
color: var(--cg-primary);
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cg-log-details summary:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cg-log-data {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
background: var(--cg-bg-primary);
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
font-size: 12px;
|
||||
color: var(--cg-text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Scrollbar styling for log container */
|
||||
.cg-log-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.cg-log-container::-webkit-scrollbar-track {
|
||||
background: var(--cg-bg-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cg-log-container::-webkit-scrollbar-thumb {
|
||||
background: var(--cg-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.cg-log-container::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--cg-primary);
|
||||
}
|
||||
|
||||
@ -1,257 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require client-guardian/api as API';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
API.listSessions(),
|
||||
API.getPolicy(),
|
||||
API.getStatus()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var sessions = data[0] || {};
|
||||
var policy = data[1] || {};
|
||||
var status = data[2] || {};
|
||||
|
||||
var sessionList = sessions.sessions || [];
|
||||
var nds = sessions.nodogsplash || {};
|
||||
|
||||
var view = E('div', { 'class': 'cbi-map' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
E('h2', {}, _('Captive Portal Management')),
|
||||
|
||||
// Nodogsplash Status Card
|
||||
E('div', { 'class': 'cbi-section', 'style': 'background: ' + (nds.running ? '#d4edda' : '#f8d7da') + '; border-left: 4px solid ' + (nds.running ? '#28a745' : '#dc3545') + '; padding: 1em; margin-bottom: 1em;' }, [
|
||||
E('h3', { 'style': 'margin-top: 0;' }, _('Nodogsplash Status')),
|
||||
E('div', { 'style': 'display: flex; gap: 2em; align-items: center;' }, [
|
||||
E('div', {}, [
|
||||
E('strong', {}, _('Service:')),
|
||||
' ',
|
||||
E('span', { 'class': 'badge', 'style': 'background: ' + (nds.running ? '#28a745' : '#dc3545') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' },
|
||||
nds.running ? _('RUNNING') : _('STOPPED'))
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('strong', {}, _('Active Sessions:')),
|
||||
' ',
|
||||
E('span', { 'style': 'font-size: 1.5em; color: #0088cc; font-weight: bold;' }, sessionList.length.toString())
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('strong', {}, _('Default Policy:')),
|
||||
' ',
|
||||
E('span', { 'class': 'badge', 'style': 'background: #0088cc; color: white; padding: 0.25em 0.6em; border-radius: 3px;' },
|
||||
policy.default_policy || 'captive')
|
||||
])
|
||||
]),
|
||||
!nds.running ? E('p', { 'style': 'margin: 1em 0 0 0; color: #856404; background: #fff3cd; padding: 0.75em; border-radius: 4px;' }, [
|
||||
E('strong', {}, _('Note:')),
|
||||
' ',
|
||||
_('Nodogsplash is not running. Start the service to enable captive portal functionality.')
|
||||
]) : null
|
||||
]),
|
||||
|
||||
// Active Sessions Table
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;' }, [
|
||||
E('h3', { 'style': 'margin: 0;' }, _('Active Portal Sessions')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'click': L.bind(this.handleRefresh, this)
|
||||
}, _('Refresh'))
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'table-wrapper' }, [
|
||||
E('table', { 'class': 'table', 'id': 'sessions-table' }, [
|
||||
E('thead', {}, [
|
||||
E('tr', {}, [
|
||||
E('th', {}, _('MAC Address')),
|
||||
E('th', {}, _('IP Address')),
|
||||
E('th', {}, _('Hostname')),
|
||||
E('th', {}, _('Duration')),
|
||||
E('th', { 'style': 'text-align: right;' }, _('Downloaded')),
|
||||
E('th', { 'style': 'text-align: right;' }, _('Uploaded')),
|
||||
E('th', {}, _('State')),
|
||||
E('th', { 'class': 'cbi-section-actions' }, _('Actions'))
|
||||
])
|
||||
]),
|
||||
E('tbody', { 'id': 'sessions-tbody' },
|
||||
this.renderSessionRows(sessionList)
|
||||
)
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Help Section
|
||||
E('div', { 'class': 'cbi-section', 'style': 'background: #e8f4f8; padding: 1em; margin-top: 2em;' }, [
|
||||
E('h3', {}, _('Captive Portal Information')),
|
||||
E('p', {}, _('The captive portal intercepts new clients and requires them to authenticate before accessing the network. Sessions are managed by nodogsplash.')),
|
||||
E('ul', {}, [
|
||||
E('li', {}, _('Active sessions show clients currently authenticated through the portal')),
|
||||
E('li', {}, _('Use "Deauthorize" to end a session and force re-authentication')),
|
||||
E('li', {}, _('Configure portal settings in the Portal tab')),
|
||||
E('li', {}, _('Change default policy in Settings to control portal behavior'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
// Setup auto-refresh
|
||||
poll.add(L.bind(function() {
|
||||
return API.listSessions().then(L.bind(function(refreshData) {
|
||||
var tbody = document.getElementById('sessions-tbody');
|
||||
if (tbody) {
|
||||
var refreshedSessions = refreshData.sessions || [];
|
||||
dom.content(tbody, this.renderSessionRows(refreshedSessions));
|
||||
}
|
||||
}, this));
|
||||
}, this), 5);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
renderSessionRows: function(sessions) {
|
||||
if (!sessions || sessions.length === 0) {
|
||||
return E('tr', {}, [
|
||||
E('td', { 'colspan': 8, 'style': 'text-align: center; padding: 2em; color: #999;' },
|
||||
_('No active captive portal sessions'))
|
||||
]);
|
||||
}
|
||||
|
||||
return sessions.map(L.bind(function(session) {
|
||||
return E('tr', {}, [
|
||||
E('td', {}, [
|
||||
E('code', { 'style': 'font-size: 0.9em;' }, session.mac || 'N/A')
|
||||
]),
|
||||
E('td', {}, session.ip || 'N/A'),
|
||||
E('td', {}, session.hostname || 'Unknown'),
|
||||
E('td', {}, this.formatDuration(session.duration || 0)),
|
||||
E('td', { 'style': 'text-align: right; font-family: monospace;' },
|
||||
this.formatBytes(session.downloaded || 0)),
|
||||
E('td', { 'style': 'text-align: right; font-family: monospace;' },
|
||||
this.formatBytes(session.uploaded || 0)),
|
||||
E('td', {}, [
|
||||
E('span', {
|
||||
'class': 'badge',
|
||||
'style': 'background: #28a745; color: white; padding: 0.25em 0.6em; border-radius: 3px;'
|
||||
}, session.state || 'authenticated')
|
||||
]),
|
||||
E('td', { 'class': 'cbi-section-actions' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-negative cbi-button-remove',
|
||||
'click': L.bind(function(ev) {
|
||||
this.handleDeauth(ev, session.mac, session.ip, session.hostname);
|
||||
}, this)
|
||||
}, _('Deauthorize'))
|
||||
])
|
||||
]);
|
||||
}, this));
|
||||
},
|
||||
|
||||
formatDuration: function(seconds) {
|
||||
if (!seconds || seconds === 0) return '0s';
|
||||
|
||||
var hours = Math.floor(seconds / 3600);
|
||||
var minutes = Math.floor((seconds % 3600) / 60);
|
||||
var secs = seconds % 60;
|
||||
|
||||
var parts = [];
|
||||
if (hours > 0) parts.push(hours + 'h');
|
||||
if (minutes > 0) parts.push(minutes + 'm');
|
||||
if (secs > 0 || parts.length === 0) parts.push(secs + 's');
|
||||
|
||||
return parts.join(' ');
|
||||
},
|
||||
|
||||
formatBytes: function(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
|
||||
var units = ['B', 'KB', 'MB', 'GB'];
|
||||
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
i = Math.min(i, units.length - 1);
|
||||
|
||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
||||
},
|
||||
|
||||
handleDeauth: function(ev, mac, ip, hostname) {
|
||||
var btn = ev.target;
|
||||
|
||||
ui.showModal(_('Deauthorize Client'), [
|
||||
E('p', {}, _('Are you sure you want to deauthorize this client?')),
|
||||
E('div', { 'style': 'background: #f8f9fa; padding: 1em; margin: 1em 0; border-radius: 4px;' }, [
|
||||
E('div', {}, [E('strong', {}, _('Hostname:')), ' ', hostname || 'Unknown']),
|
||||
E('div', {}, [E('strong', {}, _('MAC:')), ' ', E('code', {}, mac)]),
|
||||
E('div', {}, [E('strong', {}, _('IP:')), ' ', ip || 'N/A'])
|
||||
]),
|
||||
E('p', {}, _('The client will be immediately disconnected and must re-authenticate through the captive portal.')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-negative',
|
||||
'click': L.bind(function() {
|
||||
ui.hideModal();
|
||||
this.deauthorize(mac, ip, btn);
|
||||
}, this)
|
||||
}, _('Deauthorize'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
deauthorize: function(mac, ip, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = _('Deauthorizing...');
|
||||
|
||||
API.deauthorizeClient(mac, ip).then(L.bind(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null,
|
||||
E('p', _('Client %s has been deauthorized').format(mac)),
|
||||
'info'
|
||||
);
|
||||
|
||||
// Refresh the table
|
||||
this.handleRefresh();
|
||||
} else {
|
||||
ui.addNotification(null,
|
||||
E('p', _('Failed to deauthorize client: %s').format(result.error || 'Unknown error')),
|
||||
'error'
|
||||
);
|
||||
btn.disabled = false;
|
||||
btn.textContent = _('Deauthorize');
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
ui.addNotification(null,
|
||||
E('p', _('Error: %s').format(err.message || err)),
|
||||
'error'
|
||||
);
|
||||
btn.disabled = false;
|
||||
btn.textContent = _('Deauthorize');
|
||||
});
|
||||
},
|
||||
|
||||
handleRefresh: function() {
|
||||
poll.start();
|
||||
|
||||
return Promise.all([
|
||||
API.listSessions(),
|
||||
API.getPolicy()
|
||||
]).then(L.bind(function(data) {
|
||||
var tbody = document.getElementById('sessions-tbody');
|
||||
if (tbody) {
|
||||
var sessions = data[0].sessions || [];
|
||||
dom.content(tbody, this.renderSessionRows(sessions));
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -15,8 +15,8 @@ return view.extend({
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var clients = data[0].clients || [];
|
||||
var zones = data[1].zones || [];
|
||||
var clients = Array.isArray(data[0]) ? data[0] : (data[0].clients || []);
|
||||
var zones = Array.isArray(data[1]) ? data[1] : (data[1].zones || []);
|
||||
var self = this;
|
||||
|
||||
var view = E('div', { 'class': 'client-guardian-dashboard' }, [
|
||||
@ -188,15 +188,16 @@ return view.extend({
|
||||
]),
|
||||
E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end' }, [
|
||||
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
|
||||
E('button', { 'class': 'cg-btn cg-btn-success', 'click': function() {
|
||||
E('button', { 'class': 'cg-btn cg-btn-success', 'click': L.bind(function() {
|
||||
var name = document.getElementById('approve-name').value;
|
||||
var zone = document.getElementById('approve-zone').value;
|
||||
var notes = document.getElementById('approve-notes').value;
|
||||
api.approveClient(mac, name, zone, notes).then(function() {
|
||||
api.approveClient(mac, name, zone, notes).then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
});
|
||||
}}, _('Approuver'))
|
||||
ui.addNotification(null, E('p', _('Client approved successfully')), 'success');
|
||||
this.handleRefresh();
|
||||
}, this));
|
||||
}, this)}, _('Approuver'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
@ -229,7 +230,7 @@ return view.extend({
|
||||
]),
|
||||
E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end' }, [
|
||||
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
|
||||
E('button', { 'class': 'cg-btn cg-btn-primary', 'click': function() {
|
||||
E('button', { 'class': 'cg-btn cg-btn-primary', 'click': L.bind(function() {
|
||||
api.updateClient(
|
||||
client.section,
|
||||
document.getElementById('edit-name').value,
|
||||
@ -237,11 +238,12 @@ return view.extend({
|
||||
document.getElementById('edit-notes').value,
|
||||
parseInt(document.getElementById('edit-quota').value) || 0,
|
||||
document.getElementById('edit-ip').value
|
||||
).then(function() {
|
||||
).then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
});
|
||||
}}, _('Enregistrer'))
|
||||
ui.addNotification(null, E('p', _('Client updated successfully')), 'success');
|
||||
this.handleRefresh();
|
||||
}, this));
|
||||
}, this)}, _('Enregistrer'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
@ -258,26 +260,39 @@ return view.extend({
|
||||
]),
|
||||
E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end' }, [
|
||||
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
|
||||
E('button', { 'class': 'cg-btn cg-btn-danger', 'click': function() {
|
||||
E('button', { 'class': 'cg-btn cg-btn-danger', 'click': L.bind(function() {
|
||||
var reason = document.getElementById('ban-reason').value || 'Manual ban';
|
||||
api.banClient(mac, reason).then(function() {
|
||||
api.banClient(mac, reason).then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
});
|
||||
}}, _('Bannir'))
|
||||
ui.addNotification(null, E('p', _('Client banned successfully')), 'info');
|
||||
this.handleRefresh();
|
||||
}, this));
|
||||
}, this)}, _('Bannir'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleUnban: function(ev) {
|
||||
var mac = ev.currentTarget.dataset.mac;
|
||||
api.quarantineClient(mac).then(function() {
|
||||
window.location.reload();
|
||||
});
|
||||
api.quarantineClient(mac).then(L.bind(function() {
|
||||
ui.addNotification(null, E('p', _('Client unbanned successfully')), 'success');
|
||||
this.handleRefresh();
|
||||
}, this));
|
||||
},
|
||||
|
||||
handleRefresh: function() {
|
||||
window.location.reload();
|
||||
return Promise.all([
|
||||
api.getClients(),
|
||||
api.getZones()
|
||||
]).then(L.bind(function(data) {
|
||||
var container = document.querySelector('.client-guardian-dashboard');
|
||||
if (container) {
|
||||
var newView = this.render(data);
|
||||
dom.content(container.parentNode, newView);
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
console.error('Failed to refresh clients list:', err);
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
|
||||
@ -12,14 +12,15 @@ return view.extend({
|
||||
return Promise.all([
|
||||
api.getStatus(),
|
||||
api.getClients(),
|
||||
api.getZones()
|
||||
api.getZones(),
|
||||
uci.load('client-guardian')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var status = data[0];
|
||||
var clients = data[1].clients || [];
|
||||
var zones = data[2].zones || [];
|
||||
var clients = Array.isArray(data[1]) ? data[1] : (data[1].clients || []);
|
||||
var zones = Array.isArray(data[2]) ? data[2] : (data[2].zones || []);
|
||||
|
||||
var onlineClients = clients.filter(function(c) { return c.online; });
|
||||
var approvedClients = clients.filter(function(c) { return c.status === 'approved'; });
|
||||
@ -51,8 +52,8 @@ return view.extend({
|
||||
this.renderStatCard('✅', approvedClients.length, 'Approuvés'),
|
||||
this.renderStatCard('⏳', quarantineClients.length, 'Quarantaine'),
|
||||
this.renderStatCard('🚫', bannedClients.length, 'Bannis'),
|
||||
this.renderStatCard('🌐', zones.length, 'Zones'),
|
||||
this.renderStatCard('🔔', status.alerts_today || 0, 'Alertes Aujourd\'hui')
|
||||
this.renderStatCard('⚠️', clients.filter(function(c) { return c.has_threats; }).length, 'Menaces Actives'),
|
||||
this.renderStatCard('🌐', zones.length, 'Zones')
|
||||
]),
|
||||
|
||||
// Recent Clients Card
|
||||
@ -88,6 +89,16 @@ return view.extend({
|
||||
]) : E('div')
|
||||
]);
|
||||
|
||||
// Setup auto-refresh polling based on UCI settings
|
||||
var autoRefresh = uci.get('client-guardian', 'config', 'auto_refresh');
|
||||
var refreshInterval = parseInt(uci.get('client-guardian', 'config', 'refresh_interval') || '10');
|
||||
|
||||
if (autoRefresh === '1') {
|
||||
poll.add(L.bind(function() {
|
||||
return this.handleRefresh();
|
||||
}, this), refreshInterval);
|
||||
}
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
@ -114,11 +125,19 @@ return view.extend({
|
||||
E('div', { 'class': 'cg-client-info' }, [
|
||||
E('div', { 'class': 'cg-client-name' }, [
|
||||
client.online ? E('span', { 'class': 'online-indicator' }) : E('span'),
|
||||
client.name || client.hostname || 'Unknown'
|
||||
client.name || client.hostname || 'Unknown',
|
||||
client.has_threats ? E('span', {
|
||||
'class': 'cg-threat-badge',
|
||||
'title': (client.threat_count || 0) + ' menace(s) active(s), score de risque: ' + (client.risk_score || 0),
|
||||
'style': 'margin-left: 8px; color: #ef4444; font-size: 16px; cursor: help;'
|
||||
}, '⚠️') : E('span')
|
||||
]),
|
||||
E('div', { 'class': 'cg-client-meta' }, [
|
||||
E('span', {}, client.mac),
|
||||
E('span', {}, client.ip || 'N/A')
|
||||
E('span', {}, client.ip || 'N/A'),
|
||||
client.has_threats ? E('span', {
|
||||
'style': 'color: #ef4444; font-weight: 500; margin-left: 8px;'
|
||||
}, 'Risque: ' + (client.risk_score || 0) + '%') : E('span')
|
||||
])
|
||||
]),
|
||||
E('span', { 'class': 'cg-client-zone ' + zoneClass }, client.zone || 'unknown'),
|
||||
@ -176,13 +195,14 @@ return view.extend({
|
||||
}, _('Annuler')),
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-success',
|
||||
'click': function() {
|
||||
'click': L.bind(function() {
|
||||
var zone = document.getElementById('approve-zone').value;
|
||||
api.approveClient(mac, '', zone, '').then(function() {
|
||||
api.approveClient(mac, '', zone, '').then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
ui.addNotification(null, E('p', _('Client approved successfully')), 'success');
|
||||
this.handleRefresh();
|
||||
}, this));
|
||||
}, this)
|
||||
}, _('Approuver'))
|
||||
])
|
||||
]);
|
||||
@ -201,17 +221,52 @@ return view.extend({
|
||||
}, _('Annuler')),
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-danger',
|
||||
'click': function() {
|
||||
api.banClient(mac, 'Manual ban').then(function() {
|
||||
'click': L.bind(function() {
|
||||
api.banClient(mac, 'Manual ban').then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
ui.addNotification(null, E('p', _('Client banned successfully')), 'info');
|
||||
this.handleRefresh();
|
||||
}, this));
|
||||
}, this)
|
||||
}, _('Bannir'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleRefresh: function() {
|
||||
return Promise.all([
|
||||
api.getStatus(),
|
||||
api.getClients(),
|
||||
api.getZones()
|
||||
]).then(L.bind(function(data) {
|
||||
// Update dashboard without full page reload
|
||||
var container = document.querySelector('.client-guardian-dashboard');
|
||||
if (container) {
|
||||
// Show loading indicator
|
||||
var statusBadge = document.querySelector('.cg-status-badge');
|
||||
if (statusBadge) {
|
||||
statusBadge.classList.add('loading');
|
||||
}
|
||||
|
||||
// Reconstruct data array (status, clients, zones, uci already loaded)
|
||||
var newView = this.render(data);
|
||||
dom.content(container.parentNode, newView);
|
||||
|
||||
// Remove loading indicator
|
||||
if (statusBadge) {
|
||||
statusBadge.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
console.error('Failed to refresh Client Guardian dashboard:', err);
|
||||
});
|
||||
},
|
||||
|
||||
handleLeave: function() {
|
||||
// Stop polling when leaving the view to prevent memory leaks
|
||||
poll.stop();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
|
||||
@ -1,215 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require secubox-theme/theme as Theme';
|
||||
'require dom';
|
||||
'require ui';
|
||||
'require client-guardian.api as api';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return api.getPortal();
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var portal = data;
|
||||
var self = this;
|
||||
|
||||
return E('div', { 'class': 'client-guardian-dashboard' }, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('client-guardian/dashboard.css') }),
|
||||
|
||||
E('div', { 'class': 'cg-header' }, [
|
||||
E('div', { 'class': 'cg-logo' }, [
|
||||
E('div', { 'class': 'cg-logo-icon' }, '🚪'),
|
||||
E('div', { 'class': 'cg-logo-text' }, 'Portail Captif')
|
||||
]),
|
||||
E('div', { 'class': 'cg-status-badge ' + (portal.enabled ? 'approved' : 'offline') }, [
|
||||
E('span', { 'class': 'cg-status-dot' }),
|
||||
portal.enabled ? 'Actif' : 'Inactif'
|
||||
])
|
||||
]),
|
||||
|
||||
// Configuration
|
||||
E('div', { 'class': 'cg-card' }, [
|
||||
E('div', { 'class': 'cg-card-header' }, [
|
||||
E('div', { 'class': 'cg-card-title' }, [
|
||||
E('span', { 'class': 'cg-card-title-icon' }, '⚙️'),
|
||||
'Configuration'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cg-card-body' }, [
|
||||
E('div', { 'class': 'cg-toggle' }, [
|
||||
E('div', { 'class': 'cg-toggle-info' }, [
|
||||
E('span', { 'class': 'cg-toggle-icon' }, '🚪'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'cg-toggle-label' }, 'Portail Captif'),
|
||||
E('div', { 'class': 'cg-toggle-desc' }, 'Activer pour les zones Guest et Quarantine')
|
||||
])
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'cg-toggle-switch' + (portal.enabled ? ' active' : ''),
|
||||
'id': 'toggle-portal',
|
||||
'click': function() { this.classList.toggle('active'); }
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'cg-toggle' }, [
|
||||
E('div', { 'class': 'cg-toggle-info' }, [
|
||||
E('span', { 'class': 'cg-toggle-icon' }, '📝'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'cg-toggle-label' }, 'Conditions d\'utilisation'),
|
||||
E('div', { 'class': 'cg-toggle-desc' }, 'Exiger l\'acceptation des CGU')
|
||||
])
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'cg-toggle-switch' + (portal.require_terms ? ' active' : ''),
|
||||
'id': 'toggle-terms',
|
||||
'click': function() { this.classList.toggle('active'); }
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'cg-toggle' }, [
|
||||
E('div', { 'class': 'cg-toggle-info' }, [
|
||||
E('span', { 'class': 'cg-toggle-icon' }, '📧'),
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'cg-toggle-label' }, 'Inscription'),
|
||||
E('div', { 'class': 'cg-toggle-desc' }, 'Permettre l\'auto-inscription (avec approbation)')
|
||||
])
|
||||
]),
|
||||
E('div', {
|
||||
'class': 'cg-toggle-switch' + (portal.allow_registration ? ' active' : ''),
|
||||
'id': 'toggle-registration',
|
||||
'click': function() { this.classList.toggle('active'); }
|
||||
})
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'cg-form-group', 'style': 'margin-top: 20px' }, [
|
||||
E('label', { 'class': 'cg-form-label' }, 'Titre du Portail'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'portal-title',
|
||||
'class': 'cg-input',
|
||||
'value': portal.title || 'Bienvenue sur le Réseau'
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'cg-form-group' }, [
|
||||
E('label', { 'class': 'cg-form-label' }, 'Sous-titre'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'portal-subtitle',
|
||||
'class': 'cg-input',
|
||||
'value': portal.subtitle || 'Veuillez vous identifier pour accéder à Internet'
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'cg-form-group' }, [
|
||||
E('label', { 'class': 'cg-form-label' }, 'Méthode d\'authentification'),
|
||||
E('select', { 'id': 'portal-auth', 'class': 'cg-input' }, [
|
||||
E('option', { 'value': 'password', 'selected': portal.auth_method === 'password' }, 'Mot de passe unique'),
|
||||
E('option', { 'value': 'voucher', 'selected': portal.auth_method === 'voucher' }, 'Codes voucher'),
|
||||
E('option', { 'value': 'click', 'selected': portal.auth_method === 'click' }, 'Click-through (acceptation CGU)')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cg-form-group' }, [
|
||||
E('label', { 'class': 'cg-form-label' }, 'Mot de passe Invité'),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'id': 'portal-password',
|
||||
'class': 'cg-input',
|
||||
'value': portal.guest_password || 'guest2024'
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'cg-form-group' }, [
|
||||
E('label', { 'class': 'cg-form-label' }, 'Couleur d\'accent'),
|
||||
E('input', {
|
||||
'type': 'color',
|
||||
'id': 'portal-color',
|
||||
'class': 'cg-input',
|
||||
'style': 'width: 80px; height: 40px; padding: 4px',
|
||||
'value': portal.accent_color || '#ef4444'
|
||||
})
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'cg-btn-group' }, [
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-primary',
|
||||
'click': L.bind(this.handleSavePortal, this)
|
||||
}, [
|
||||
E('span', {}, '💾'),
|
||||
' Enregistrer'
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Portal Preview
|
||||
E('div', { 'class': 'cg-card' }, [
|
||||
E('div', { 'class': 'cg-card-header' }, [
|
||||
E('div', { 'class': 'cg-card-title' }, [
|
||||
E('span', { 'class': 'cg-card-title-icon' }, '👁️'),
|
||||
'Aperçu du Portail'
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cg-card-body' }, [
|
||||
E('div', { 'class': 'cg-portal-preview', 'id': 'portal-preview' }, [
|
||||
E('div', {
|
||||
'class': 'cg-portal-preview-logo',
|
||||
'style': 'background: ' + (portal.accent_color || '#ef4444')
|
||||
}, '🛡️'),
|
||||
E('div', { 'class': 'cg-portal-preview-title', 'id': 'preview-title' },
|
||||
portal.title || 'Bienvenue sur le Réseau'
|
||||
),
|
||||
E('div', { 'class': 'cg-portal-preview-subtitle', 'id': 'preview-subtitle' },
|
||||
portal.subtitle || 'Veuillez vous identifier pour accéder à Internet'
|
||||
),
|
||||
E('input', {
|
||||
'type': 'password',
|
||||
'class': 'cg-portal-preview-input',
|
||||
'placeholder': 'Mot de passe invité'
|
||||
}),
|
||||
E('button', {
|
||||
'class': 'cg-portal-preview-btn',
|
||||
'id': 'preview-btn',
|
||||
'style': 'background: ' + (portal.accent_color || '#ef4444')
|
||||
}, 'Se Connecter')
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Active Sessions
|
||||
E('div', { 'class': 'cg-card' }, [
|
||||
E('div', { 'class': 'cg-card-header' }, [
|
||||
E('div', { 'class': 'cg-card-title' }, [
|
||||
E('span', { 'class': 'cg-card-title-icon' }, '👥'),
|
||||
'Sessions Actives'
|
||||
]),
|
||||
E('span', { 'class': 'cg-card-badge' }, (portal.active_sessions || 0) + ' sessions')
|
||||
]),
|
||||
E('div', { 'class': 'cg-card-body' }, [
|
||||
portal.active_sessions > 0 ?
|
||||
E('p', {}, 'Liste des sessions actives...') :
|
||||
E('div', { 'class': 'cg-empty-state' }, [
|
||||
E('div', { 'class': 'cg-empty-state-icon' }, '🔒'),
|
||||
E('div', { 'class': 'cg-empty-state-title' }, 'Aucune session active'),
|
||||
E('div', { 'class': 'cg-empty-state-text' }, 'Les sessions du portail captif apparaîtront ici')
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSavePortal: function(ev) {
|
||||
var title = document.getElementById('portal-title').value;
|
||||
var subtitle = document.getElementById('portal-subtitle').value;
|
||||
var auth = document.getElementById('portal-auth').value;
|
||||
var password = document.getElementById('portal-password').value;
|
||||
var color = document.getElementById('portal-color').value;
|
||||
|
||||
api.updatePortal(title, subtitle, color, auth, password).then(function(res) {
|
||||
if (res.success) {
|
||||
ui.addNotification(null, E('p', {}, _('Configuration du portail enregistrée')), 'success');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@ -22,7 +22,7 @@ return view.extend({
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('client-guardian', _('Client Guardian Settings'),
|
||||
_('Configure default network access policy and captive portal behavior.'));
|
||||
_('Configure default network access policy and client management.'));
|
||||
|
||||
// General Settings
|
||||
s = m.section(form.NamedSection, 'config', 'client-guardian', _('General Settings'));
|
||||
@ -34,9 +34,9 @@ return view.extend({
|
||||
|
||||
o = s.option(form.ListValue, 'default_policy', _('Default Policy'));
|
||||
o.value('open', _('Open - Allow all clients'));
|
||||
o.value('captive', _('Captive Portal - Require portal authentication'));
|
||||
o.value('quarantine', _('Quarantine - Require approval'));
|
||||
o.value('whitelist', _('Whitelist Only - Allow only approved clients'));
|
||||
o.default = 'captive';
|
||||
o.default = 'quarantine';
|
||||
o.rmempty = false;
|
||||
o.description = _('Default behavior for new/unknown clients');
|
||||
|
||||
@ -51,57 +51,6 @@ return view.extend({
|
||||
o.placeholder = '86400';
|
||||
o.description = _('Maximum session duration in seconds (default: 86400 = 24 hours)');
|
||||
|
||||
// Captive Portal Settings
|
||||
s = m.section(form.NamedSection, 'portal', 'portal', _('Captive Portal'));
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Captive Portal'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.description = _('Enable nodogsplash captive portal for guest authentication');
|
||||
|
||||
o = s.option(form.Value, 'title', _('Portal Title'));
|
||||
o.default = 'Welcome';
|
||||
o.placeholder = 'Welcome';
|
||||
o.description = _('Main title displayed on the portal page');
|
||||
|
||||
o = s.option(form.Value, 'subtitle', _('Portal Subtitle'));
|
||||
o.default = 'Please authenticate to access the network';
|
||||
o.placeholder = 'Please authenticate to access the network';
|
||||
o.description = _('Subtitle or welcome message');
|
||||
|
||||
o = s.option(form.ListValue, 'auth_method', _('Authentication Method'));
|
||||
o.value('click', _('Click to Continue'));
|
||||
o.value('password', _('Password'));
|
||||
o.value('voucher', _('Voucher Code'));
|
||||
o.value('email', _('Email Verification'));
|
||||
o.default = 'click';
|
||||
o.description = _('Method used to authenticate users');
|
||||
|
||||
o = s.option(form.Value, 'guest_password', _('Guest Password'));
|
||||
o.depends('auth_method', 'password');
|
||||
o.password = true;
|
||||
o.placeholder = 'Enter password';
|
||||
o.description = _('Password required for guest access');
|
||||
|
||||
o = s.option(form.Value, 'accent_color', _('Accent Color'));
|
||||
o.default = '#0088cc';
|
||||
o.placeholder = '#0088cc';
|
||||
o.datatype = 'string';
|
||||
o.description = _('Hex color code for portal branding');
|
||||
|
||||
o = s.option(form.Flag, 'require_terms', _('Require Terms Acceptance'));
|
||||
o.default = '0';
|
||||
o.description = _('Require users to accept terms and conditions');
|
||||
|
||||
o = s.option(form.Flag, 'allow_registration', _('Allow Self-Registration'));
|
||||
o.default = '0';
|
||||
o.description = _('Allow users to register their own devices');
|
||||
|
||||
o = s.option(form.Flag, 'registration_approval', _('Require Registration Approval'));
|
||||
o.default = '1';
|
||||
o.depends('allow_registration', '1');
|
||||
o.description = _('Administrator must approve self-registered devices');
|
||||
|
||||
// Advanced Settings
|
||||
s = m.section(form.NamedSection, 'config', 'client-guardian', _('Advanced Settings'));
|
||||
|
||||
@ -127,26 +76,128 @@ return view.extend({
|
||||
o.default = '0';
|
||||
o.description = _('Attempt to detect and block VPN connections');
|
||||
|
||||
// Dashboard Reactiveness
|
||||
s = m.section(form.NamedSection, 'config', 'client-guardian', _('Dashboard Reactiveness'));
|
||||
|
||||
o = s.option(form.Flag, 'auto_refresh', _('Enable Auto-Refresh'),
|
||||
_('Automatically refresh dashboard every few seconds'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'refresh_interval', _('Refresh Interval'),
|
||||
_('How often to poll for updates'));
|
||||
o.value('5', _('Every 5 seconds'));
|
||||
o.value('10', _('Every 10 seconds (recommended)'));
|
||||
o.value('30', _('Every 30 seconds'));
|
||||
o.value('60', _('Every 60 seconds'));
|
||||
o.default = '10';
|
||||
o.depends('auto_refresh', '1');
|
||||
|
||||
// Threat Intelligence Integration
|
||||
s = m.section(form.NamedSection, 'threat_policy', 'threat_policy', _('Threat Intelligence Integration'));
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable Threat Intelligence'),
|
||||
_('Correlate clients with Security Threats Dashboard data'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'auto_ban_threshold', _('Auto-Ban Threshold'),
|
||||
_('Automatically ban clients with threat score above this value (0-100)'));
|
||||
o.datatype = 'range(1,100)';
|
||||
o.placeholder = '80';
|
||||
o.default = '80';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'auto_quarantine_threshold', _('Auto-Quarantine Threshold'),
|
||||
_('Automatically quarantine clients with threat score above this value (0-100)'));
|
||||
o.datatype = 'range(1,100)';
|
||||
o.placeholder = '60';
|
||||
o.default = '60';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
o = s.option(form.Value, 'threat_check_interval', _('Threat Check Interval'),
|
||||
_('How often to check for threats (seconds)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '60';
|
||||
o.default = '60';
|
||||
o.depends('enabled', '1');
|
||||
|
||||
// Auto-Zoning / Auto-Parking
|
||||
s = m.section(form.NamedSection, 'config', 'client-guardian', _('Auto-Zoning & Auto-Parking'));
|
||||
s.description = _('Automatically assign new clients to zones based on device type, vendor, or hostname patterns.');
|
||||
|
||||
o = s.option(form.Flag, 'auto_zoning_enabled', _('Enable Auto-Zoning'),
|
||||
_('Automatically assign clients to zones using matching rules'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'auto_parking_zone', _('Auto-Parking Zone'),
|
||||
_('Default zone for clients that don\'t match any rule'));
|
||||
o.value('guest', _('Guest'));
|
||||
o.value('quarantine', _('Quarantine'));
|
||||
o.value('iot', _('IoT'));
|
||||
o.value('lan_private', _('LAN Private'));
|
||||
o.default = 'guest';
|
||||
o.depends('auto_zoning_enabled', '1');
|
||||
|
||||
o = s.option(form.Flag, 'auto_parking_approve', _('Auto-Approve Parked Clients'),
|
||||
_('Automatically approve clients placed in auto-parking zone'));
|
||||
o.default = '0';
|
||||
o.depends('auto_zoning_enabled', '1');
|
||||
|
||||
// Auto-Zoning Rules Section
|
||||
s = m.section(form.GridSection, 'auto_zone_rule', _('Auto-Zoning Rules'));
|
||||
s.anonymous = false;
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.description = _('Rules are evaluated in priority order. First match wins.');
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = '1';
|
||||
o.editable = true;
|
||||
|
||||
o = s.option(form.Value, 'name', _('Rule Name'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'match_type', _('Match Type'));
|
||||
o.value('vendor', _('Device Vendor (OUI)'));
|
||||
o.value('hostname', _('Hostname Pattern'));
|
||||
o.value('mac_prefix', _('MAC Prefix'));
|
||||
o.default = 'vendor';
|
||||
|
||||
o = s.option(form.Value, 'match_value', _('Match Value/Pattern'));
|
||||
o.placeholder = _('e.g., Xiaomi, Apple, .*camera.*, aa:bb:cc');
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'target_zone', _('Target Zone'));
|
||||
o.value('lan_private', _('LAN Private'));
|
||||
o.value('iot', _('IoT'));
|
||||
o.value('kids', _('Kids'));
|
||||
o.value('guest', _('Guest'));
|
||||
o.value('quarantine', _('Quarantine'));
|
||||
o.default = 'guest';
|
||||
|
||||
o = s.option(form.Flag, 'auto_approve', _('Auto-Approve'));
|
||||
o.default = '0';
|
||||
|
||||
o = s.option(form.Value, 'priority', _('Priority'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '50';
|
||||
o.default = '50';
|
||||
o.description = _('Lower numbers = higher priority');
|
||||
|
||||
return m.render().then(function(rendered) {
|
||||
// Add policy info box at the top
|
||||
var infoBox = E('div', {
|
||||
'class': 'cbi-section',
|
||||
'style': 'background: #e8f4f8; border-left: 4px solid #0088cc; padding: 1em; margin-bottom: 1em;'
|
||||
}, [
|
||||
E('h3', { 'style': 'margin-top: 0;' }, _('Current Policy: ') + E('span', { 'style': 'color: #0088cc;' }, policy.default_policy || 'captive')),
|
||||
E('div', { 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1em; margin-top: 1em;' }, [
|
||||
E('div', {}, [
|
||||
E('strong', {}, _('Portal Enabled:')),
|
||||
' ',
|
||||
E('span', { 'class': 'badge', 'style': 'background: ' + (policy.portal_enabled ? '#28a745' : '#6c757d') + '; color: white; padding: 0.25em 0.6em; border-radius: 3px;' },
|
||||
policy.portal_enabled ? _('Yes') : _('No'))
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('strong', {}, _('Session Timeout:')),
|
||||
' ',
|
||||
E('span', {}, (policy.session_timeout || 86400) + ' ' + _('seconds'))
|
||||
])
|
||||
]),
|
||||
E('h3', { 'style': 'margin-top: 0;' }, _('Current Policy: ') + E('span', { 'style': 'color: #0088cc;' }, policy.default_policy || 'quarantine')),
|
||||
E('div', { 'style': 'margin-top: 1em;' }, [
|
||||
E('strong', {}, _('Session Timeout:')),
|
||||
' ',
|
||||
E('span', {}, (policy.session_timeout || 86400) + ' ' + _('seconds'))
|
||||
]),
|
||||
E('div', { 'style': 'margin-top: 1em; padding: 0.75em; background: white; border-radius: 4px;' }, [
|
||||
E('strong', {}, _('Policy Descriptions:')),
|
||||
E('ul', { 'style': 'margin: 0.5em 0;' }, [
|
||||
@ -156,9 +207,9 @@ return view.extend({
|
||||
_('All clients can access the network without authentication. Not recommended for public networks.')
|
||||
]),
|
||||
E('li', {}, [
|
||||
E('strong', {}, _('Captive Portal:')),
|
||||
E('strong', {}, _('Quarantine:')),
|
||||
' ',
|
||||
_('New clients are redirected to a captive portal for authentication. Recommended for guest networks.')
|
||||
_('New clients are placed in quarantine and require manual approval. Recommended for secure networks.')
|
||||
]),
|
||||
E('li', {}, [
|
||||
E('strong', {}, _('Whitelist Only:')),
|
||||
|
||||
@ -11,7 +11,7 @@ return view.extend({
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var zones = data.zones || [];
|
||||
var zones = Array.isArray(data) ? data : (data.zones || []);
|
||||
var self = this;
|
||||
|
||||
return E('div', { 'class': 'client-guardian-dashboard' }, [
|
||||
@ -22,11 +22,19 @@ return view.extend({
|
||||
E('div', { 'class': 'cg-logo' }, [
|
||||
E('div', { 'class': 'cg-logo-icon' }, '🌐'),
|
||||
E('div', { 'class': 'cg-logo-text' }, 'Zones Réseau')
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cg-btn cg-btn-primary',
|
||||
'click': L.bind(this.handleSyncZones, this),
|
||||
'style': 'display: flex; align-items: center; gap: 8px;'
|
||||
}, [
|
||||
E('span', {}, '🔄'),
|
||||
'Synchroniser Firewall'
|
||||
])
|
||||
]),
|
||||
|
||||
E('p', { 'style': 'color: var(--cg-text-secondary); margin-bottom: 24px' },
|
||||
'Définissez les zones de sécurité avec leurs règles d\'accès, filtrage et limitations.'
|
||||
'Définissez les zones de sécurité avec leurs règles d\'accès, filtrage et limitations. Cliquez sur "Synchroniser Firewall" pour créer les zones dans la configuration firewall.'
|
||||
),
|
||||
|
||||
E('div', { 'class': 'cg-zones-grid' },
|
||||
@ -153,7 +161,7 @@ return view.extend({
|
||||
]) : E('span'),
|
||||
E('div', { 'class': 'cg-btn-group', 'style': 'justify-content: flex-end; margin-top: 20px' }, [
|
||||
E('button', { 'class': 'cg-btn', 'click': ui.hideModal }, _('Annuler')),
|
||||
E('button', { 'class': 'cg-btn cg-btn-primary', 'click': function() {
|
||||
E('button', { 'class': 'cg-btn cg-btn-primary', 'click': L.bind(function() {
|
||||
api.updateZone(
|
||||
zone.id,
|
||||
zone.name,
|
||||
@ -161,15 +169,53 @@ return view.extend({
|
||||
document.getElementById('zone-filter').value,
|
||||
zone.time_restrictions ? document.getElementById('zone-start').value : '',
|
||||
zone.time_restrictions ? document.getElementById('zone-end').value : ''
|
||||
).then(function() {
|
||||
).then(L.bind(function() {
|
||||
ui.hideModal();
|
||||
window.location.reload();
|
||||
});
|
||||
}}, _('Enregistrer'))
|
||||
ui.addNotification(null, E('p', _('Zone updated successfully')), 'success');
|
||||
this.handleRefresh();
|
||||
}, this));
|
||||
}, this)}, _('Enregistrer'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSyncZones: function(ev) {
|
||||
var btn = ev.currentTarget;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span>⏳</span> Synchronisation...';
|
||||
|
||||
api.syncZones().then(function(result) {
|
||||
if (result.success) {
|
||||
ui.addNotification(null, E('p', {}, 'Zones firewall synchronisées avec succès'), 'success');
|
||||
btn.innerHTML = '<span>✅</span> Synchronisé';
|
||||
setTimeout(function() {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>🔄</span> Synchroniser Firewall';
|
||||
}, 2000);
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, 'Erreur lors de la synchronisation'), 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>🔄</span> Synchroniser Firewall';
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.addNotification(null, E('p', {}, 'Erreur: ' + err), 'error');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<span>🔄</span> Synchroniser Firewall';
|
||||
});
|
||||
},
|
||||
|
||||
handleRefresh: function() {
|
||||
return api.getZones().then(L.bind(function(data) {
|
||||
var container = document.querySelector('.client-guardian-dashboard');
|
||||
if (container) {
|
||||
var newView = this.render(data);
|
||||
dom.content(container.parentNode, newView);
|
||||
}
|
||||
}, this)).catch(function(err) {
|
||||
console.error('Failed to refresh zones list:', err);
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
|
||||
@ -4,10 +4,18 @@ config client-guardian 'config'
|
||||
option quarantine_zone 'quarantine'
|
||||
option scan_interval '30'
|
||||
option auto_approve '0'
|
||||
option portal_enabled '1'
|
||||
option portal_url '/cgi-bin/client-guardian-portal'
|
||||
option session_timeout '3600'
|
||||
option log_level 'info'
|
||||
# Dashboard Reactiveness
|
||||
option auto_refresh '1'
|
||||
option refresh_interval '10'
|
||||
# Debug Mode
|
||||
option debug_enabled '0'
|
||||
option debug_level 'INFO'
|
||||
option enable_active_scan '1'
|
||||
# Auto-Zoning / Auto-Parking
|
||||
option auto_zoning_enabled '1'
|
||||
option auto_parking_zone 'guest'
|
||||
option auto_parking_approve '0'
|
||||
|
||||
# Alert Configuration
|
||||
config alerts 'alerts'
|
||||
@ -76,7 +84,13 @@ config zone 'kids'
|
||||
option content_filter 'kids'
|
||||
option schedule_start '08:00'
|
||||
option schedule_end '21:00'
|
||||
list schedule_days 'mon' 'tue' 'wed' 'thu' 'fri' 'sat' 'sun'
|
||||
list schedule_days 'mon'
|
||||
list schedule_days 'tue'
|
||||
list schedule_days 'wed'
|
||||
list schedule_days 'thu'
|
||||
list schedule_days 'fri'
|
||||
list schedule_days 'sat'
|
||||
list schedule_days 'sun'
|
||||
|
||||
config zone 'guest'
|
||||
option name 'Invités'
|
||||
@ -90,8 +104,6 @@ config zone 'guest'
|
||||
option bandwidth_limit '25'
|
||||
option time_restrictions '0'
|
||||
option content_filter 'adult'
|
||||
option session_duration '7200'
|
||||
option portal_required '1'
|
||||
|
||||
config zone 'quarantine'
|
||||
option name 'Quarantaine'
|
||||
@ -103,8 +115,6 @@ config zone 'quarantine'
|
||||
option local_access '0'
|
||||
option inter_client '0'
|
||||
option bandwidth_limit '1'
|
||||
option portal_required '1'
|
||||
option portal_only '1'
|
||||
|
||||
config zone 'blocked'
|
||||
option name 'Bloqué'
|
||||
@ -120,15 +130,23 @@ config zone 'blocked'
|
||||
config filter 'kids_filter'
|
||||
option name 'Filtre Enfants'
|
||||
option type 'whitelist'
|
||||
list categories 'education' 'kids' 'games_safe'
|
||||
list blocked_categories 'adult' 'violence' 'gambling' 'drugs' 'weapons'
|
||||
list categories 'education'
|
||||
list categories 'kids'
|
||||
list categories 'games_safe'
|
||||
list blocked_categories 'adult'
|
||||
list blocked_categories 'violence'
|
||||
list blocked_categories 'gambling'
|
||||
list blocked_categories 'drugs'
|
||||
list blocked_categories 'weapons'
|
||||
option safe_search '1'
|
||||
option youtube_restricted '1'
|
||||
|
||||
config filter 'adult_filter'
|
||||
option name 'Filtre Adulte'
|
||||
option type 'blacklist'
|
||||
list blocked_categories 'malware' 'phishing' 'illegal'
|
||||
list blocked_categories 'malware'
|
||||
list blocked_categories 'phishing'
|
||||
list blocked_categories 'illegal'
|
||||
option safe_search '0'
|
||||
|
||||
config filter 'strict_filter'
|
||||
@ -157,7 +175,11 @@ config schedule 'school_hours'
|
||||
option action 'block'
|
||||
option start_time '08:00'
|
||||
option end_time '16:00'
|
||||
list days 'mon' 'tue' 'wed' 'thu' 'fri'
|
||||
list days 'mon'
|
||||
list days 'tue'
|
||||
list days 'wed'
|
||||
list days 'thu'
|
||||
list days 'fri'
|
||||
|
||||
config schedule 'night_block'
|
||||
option name 'Blocage Nocturne'
|
||||
@ -165,31 +187,28 @@ config schedule 'night_block'
|
||||
option action 'block'
|
||||
option start_time '22:00'
|
||||
option end_time '07:00'
|
||||
list days 'mon' 'tue' 'wed' 'thu' 'fri' 'sat' 'sun'
|
||||
list days 'mon'
|
||||
list days 'tue'
|
||||
list days 'wed'
|
||||
list days 'thu'
|
||||
list days 'fri'
|
||||
list days 'sat'
|
||||
list days 'sun'
|
||||
|
||||
config schedule 'weekend_limit'
|
||||
option name 'Limite Weekend'
|
||||
option enabled '0'
|
||||
option action 'quota'
|
||||
option daily_quota '180'
|
||||
list days 'sat' 'sun'
|
||||
list days 'sat'
|
||||
list days 'sun'
|
||||
|
||||
# Captive Portal Configuration
|
||||
config portal 'portal'
|
||||
# Threat Intelligence Integration
|
||||
config threat_policy 'threat_policy'
|
||||
option enabled '1'
|
||||
option title 'Bienvenue sur le Réseau'
|
||||
option subtitle 'Veuillez vous identifier pour accéder à Internet'
|
||||
option logo '/luci-static/client-guardian/logo.png'
|
||||
option background_color '#0f172a'
|
||||
option accent_color '#ef4444'
|
||||
option require_terms '1'
|
||||
option terms_url '/client-guardian/terms.html'
|
||||
option auth_method 'password'
|
||||
option guest_password 'guest2024'
|
||||
option allow_registration '1'
|
||||
option registration_approval '1'
|
||||
option show_bandwidth_info '1'
|
||||
option custom_css ''
|
||||
option auto_ban_threshold '80'
|
||||
option auto_quarantine_threshold '60'
|
||||
option threat_check_interval '60'
|
||||
|
||||
# Example Known Clients
|
||||
config client 'client_example1'
|
||||
@ -229,3 +248,84 @@ config client 'client_banned'
|
||||
option first_seen '2024-12-18 03:00:00'
|
||||
option ban_reason 'Tentative intrusion'
|
||||
option ban_date '2024-12-18 03:05:00'
|
||||
|
||||
# Auto-Zoning Rules
|
||||
# Rules are evaluated in order, first match wins
|
||||
|
||||
# IoT Devices - Chinese brands
|
||||
config auto_zone_rule 'rule_xiaomi'
|
||||
option enabled '1'
|
||||
option name 'Xiaomi Devices'
|
||||
option match_type 'vendor'
|
||||
option match_value 'Xiaomi'
|
||||
option target_zone 'iot'
|
||||
option auto_approve '0'
|
||||
option priority '10'
|
||||
|
||||
config auto_zone_rule 'rule_tuya'
|
||||
option enabled '1'
|
||||
option name 'Tuya Smart Devices'
|
||||
option match_type 'vendor'
|
||||
option match_value 'Tuya'
|
||||
option target_zone 'iot'
|
||||
option auto_approve '0'
|
||||
option priority '10'
|
||||
|
||||
config auto_zone_rule 'rule_tp_link'
|
||||
option enabled '1'
|
||||
option name 'TP-Link Smart Home'
|
||||
option match_type 'vendor'
|
||||
option match_value 'TP-Link'
|
||||
option target_zone 'iot'
|
||||
option auto_approve '0'
|
||||
option priority '10'
|
||||
|
||||
# Mobile devices - Kids tablets
|
||||
config auto_zone_rule 'rule_kids_tablet'
|
||||
option enabled '1'
|
||||
option name 'Kids Tablets'
|
||||
option match_type 'hostname'
|
||||
option match_pattern 'tablet-.*|.*-kid.*|samsung-tab-kid'
|
||||
option target_zone 'kids'
|
||||
option auto_approve '1'
|
||||
option priority '20'
|
||||
|
||||
# Guest devices - Temporary
|
||||
config auto_zone_rule 'rule_guest_android'
|
||||
option enabled '1'
|
||||
option name 'Guest Android Phones'
|
||||
option match_type 'hostname'
|
||||
option match_pattern 'android-.*|Galaxy-.*|Pixel-.*'
|
||||
option target_zone 'guest'
|
||||
option auto_approve '0'
|
||||
option priority '30'
|
||||
|
||||
config auto_zone_rule 'rule_guest_iphone'
|
||||
option enabled '1'
|
||||
option name 'Guest iPhones'
|
||||
option match_type 'hostname'
|
||||
option match_pattern 'iPhone.*|iPad.*'
|
||||
option target_zone 'guest'
|
||||
option auto_approve '0'
|
||||
option priority '30'
|
||||
|
||||
# Trusted devices - Apple ecosystem
|
||||
config auto_zone_rule 'rule_apple_trusted'
|
||||
option enabled '0'
|
||||
option name 'Apple Devices (Trusted)'
|
||||
option match_type 'vendor'
|
||||
option match_value 'Apple'
|
||||
option target_zone 'lan_private'
|
||||
option auto_approve '1'
|
||||
option priority '40'
|
||||
|
||||
# IoT Cameras
|
||||
config auto_zone_rule 'rule_cameras'
|
||||
option enabled '1'
|
||||
option name 'IP Cameras'
|
||||
option match_type 'hostname'
|
||||
option match_pattern '.*camera.*|.*cam.*|ipcam.*|IPCam.*'
|
||||
option target_zone 'iot'
|
||||
option auto_approve '0'
|
||||
option priority '15'
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,14 @@
|
||||
"path": "client-guardian/overview"
|
||||
}
|
||||
},
|
||||
"admin/secubox/security/client-guardian/wizard": {
|
||||
"title": "Setup Wizard",
|
||||
"order": 15,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "client-guardian/wizard"
|
||||
}
|
||||
},
|
||||
"admin/secubox/security/client-guardian/clients": {
|
||||
"title": "Clients",
|
||||
"order": 20,
|
||||
@ -80,5 +88,13 @@
|
||||
"type": "view",
|
||||
"path": "client-guardian/settings"
|
||||
}
|
||||
},
|
||||
"admin/secubox/security/client-guardian/debug": {
|
||||
"title": "Debug Console",
|
||||
"order": 95,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "client-guardian/debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Grant access to LuCI Client Guardian Dashboard",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.client-guardian": [ "status", "clients", "get_client", "zones", "parental", "portal", "alerts", "logs", "list_sessions", "get_policy" ],
|
||||
"luci.client-guardian": [ "status", "clients", "get_client", "zones", "parental", "portal", "alerts", "logs", "list_sessions", "get_policy", "list_profiles" ],
|
||||
"system": [ "info", "board" ],
|
||||
"network.interface": [ "status", "dump" ],
|
||||
"file": [ "read", "stat" ]
|
||||
@ -18,9 +18,10 @@
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.client-guardian": [ "approve_client", "ban_client", "quarantine_client", "update_client", "update_zone", "update_portal", "send_test_alert", "set_policy", "authorize_client", "deauthorize_client" ]
|
||||
"luci.client-guardian": [ "approve_client", "ban_client", "quarantine_client", "update_client", "update_zone", "update_portal", "send_test_alert", "set_policy", "authorize_client", "deauthorize_client", "sync_zones", "apply_profile" ],
|
||||
"uci": [ "apply", "commit", "set", "delete", "add", "reorder", "changes" ]
|
||||
},
|
||||
"uci": [ "client-guardian" ]
|
||||
"uci": [ "client-guardian", "firewall" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user