import { Component, createSignal, Show, onMount } from 'solid-js'; import { Portal } from 'solid-js/web'; import { SectionHeader } from '@/components/shared/SectionHeader'; interface SecurityStatus { hasAuthentication: boolean; hasHTTPS: boolean; hasAPIToken: boolean; hasAuditLogging: boolean; credentialsEncrypted: boolean; exportProtected: boolean; score: number; maxScore: number; publicAccess?: boolean; isPrivateNetwork?: boolean; clientIP?: string; } export const SecurityWarning: Component = () => { const [dismissed, setDismissed] = createSignal(false); const [status, setStatus] = createSignal(null); const [showDetails, setShowDetails] = createSignal(false); onMount(async () => { // Check if user has previously dismissed const dismissedUntil = localStorage.getItem('securityWarningDismissed'); if (dismissedUntil) { const dismissDate = new Date(dismissedUntil); if (dismissDate > new Date()) { setDismissed(true); return; } } // Fetch security status try { const response = await fetch('/api/security/status'); if (response.ok) { const data = await response.json(); // Calculate security score let score = 0; const maxScore = 5; if (data.credentialsEncrypted !== false) score++; // Always true currently if (data.exportProtected) score++; if (data.apiTokenConfigured) score++; if (data.hasHTTPS || window.location.protocol === 'https:') score++; if (data.hasAuthentication) score++; setStatus({ hasAuthentication: data.hasAuthentication || false, hasHTTPS: window.location.protocol === 'https:', hasAPIToken: data.apiTokenConfigured || false, hasAuditLogging: data.hasAuditLogging || false, credentialsEncrypted: true, // Always true in current implementation exportProtected: data.exportProtected || false, score, maxScore, publicAccess: data.publicAccess || false, isPrivateNetwork: data.isPrivateNetwork, clientIP: data.clientIP, }); } } catch (error) { console.error('Failed to fetch security status:', error); } }); const handleDismiss = (duration: 'day' | 'week' | 'forever') => { const now = new Date(); if (duration === 'day') { now.setDate(now.getDate() + 1); } else if (duration === 'week') { now.setDate(now.getDate() + 7); } else { now.setFullYear(now.getFullYear() + 100); // "Forever" } localStorage.setItem('securityWarningDismissed', now.toISOString()); setDismissed(true); }; const getScoreColor = (score: number, max: number) => { const percentage = (score / max) * 100; if (percentage >= 80) return 'text-green-600 dark:text-green-400'; if (percentage >= 60) return 'text-yellow-600 dark:text-yellow-400'; if (percentage >= 40) return 'text-orange-600 dark:text-orange-400'; return 'text-red-600 dark:text-red-400'; }; const getScoreEmoji = (score: number, max: number) => { const percentage = (score / max) * 100; if (percentage >= 80) return '🛡️'; if (percentage >= 60) return '⚠️'; return '🚨'; }; // Show more aggressively if public access detected const shouldShow = () => { if (dismissed()) return false; if (!status()) return false; // Always show if public access without auth if (status()!.publicAccess && !status()!.hasAuthentication) { return true; } // Show if score is low return status()!.score < 4; }; if (!shouldShow()) { return null; } return (
{getScoreEmoji(status()!.score, status()!.maxScore)}
Security score:{' '} {status()!.score}/{status()!.maxScore} } size="sm" class="flex-1" titleClass="text-gray-900 dark:text-gray-100" />

{status()!.publicAccess ? ( ⚠️ PUBLIC NETWORK ACCESS DETECTED - Your Proxmox credentials are exposed to the internet! ) : ( 'Your Pulse instance is accessible without authentication. Proxmox credentials could be exposed.' )}

{status()!.credentialsEncrypted ? '✅' : '❌'} Credentials encrypted at rest
{status()!.exportProtected ? '✅' : '❌'} Export requires authentication
{status()!.hasAuthentication ? '✅' : '❌'} Authentication enabled
{status()!.hasHTTPS ? '✅' : '❌'} HTTPS connection
{status()!.hasAuditLogging ? '✅' : '❌'} Audit logging enabled
Enable Security → Learn More
); };