enhance(settings): Improve SecurityPostureSummary with score indicator

- Added dynamic security score calculation (0-100%)
- Added gradient header that changes color based on score:
  - Green (>=80%): Strong security
  - Amber (50-79%): Moderate security
  - Red (<50%): Weak security
- Improved visual hierarchy with color-coded status cards
- Added 'Critical' labels for important missing features
- Changed icons to lucide-solid for consistency
This commit is contained in:
rcourtman 2025-12-13 07:54:09 +00:00
parent 6a3c143740
commit de2f6e0edb

View file

@ -1,6 +1,10 @@
import { Component, For, Show } from 'solid-js'; import { Component, For, Show, createMemo } from 'solid-js';
import { Card } from '@/components/shared/Card'; import { Card } from '@/components/shared/Card';
import { SectionHeader } from '@/components/shared/SectionHeader'; import Shield from 'lucide-solid/icons/shield';
import ShieldCheck from 'lucide-solid/icons/shield-check';
import ShieldAlert from 'lucide-solid/icons/shield-alert';
import CheckCircle from 'lucide-solid/icons/check-circle';
import XCircle from 'lucide-solid/icons/x-circle';
interface SecurityPostureSummaryProps { interface SecurityPostureSummaryProps {
status: { status: {
@ -26,24 +30,28 @@ export const SecurityPostureSummary: Component<SecurityPostureSummaryProps> = (p
label: 'Password login', label: 'Password login',
enabled: props.status.hasAuthentication, enabled: props.status.hasAuthentication,
description: props.status.hasAuthentication ? 'Active' : 'Not configured', description: props.status.hasAuthentication ? 'Active' : 'Not configured',
critical: true, // Critical security feature
}, },
{ {
key: 'oidc', key: 'oidc',
label: 'Single sign-on', label: 'Single sign-on',
enabled: Boolean(props.status.oidcEnabled), enabled: Boolean(props.status.oidcEnabled),
description: props.status.oidcEnabled ? 'OIDC configured' : 'Not configured', description: props.status.oidcEnabled ? 'OIDC configured' : 'Not configured',
critical: false,
}, },
{ {
key: 'proxy', key: 'proxy',
label: 'Proxy auth', label: 'Proxy auth',
enabled: Boolean(props.status.hasProxyAuth), enabled: Boolean(props.status.hasProxyAuth),
description: props.status.hasProxyAuth ? 'Active' : 'Not configured', description: props.status.hasProxyAuth ? 'Active' : 'Not configured',
critical: false,
}, },
{ {
key: 'token', key: 'token',
label: 'API token', label: 'API token',
enabled: props.status.apiTokenConfigured, enabled: props.status.apiTokenConfigured,
description: props.status.apiTokenConfigured ? 'Active' : 'Not configured', description: props.status.apiTokenConfigured ? 'Active' : 'Not configured',
critical: false,
}, },
{ {
key: 'export', key: 'export',
@ -52,79 +60,138 @@ export const SecurityPostureSummary: Component<SecurityPostureSummaryProps> = (p
description: props.status.unprotectedExportAllowed description: props.status.unprotectedExportAllowed
? 'Unprotected' ? 'Unprotected'
: 'Token + passphrase required', : 'Token + passphrase required',
critical: true,
}, },
{ {
key: 'https', key: 'https',
label: 'HTTPS', label: 'HTTPS',
enabled: Boolean(props.status.hasHTTPS), enabled: Boolean(props.status.hasHTTPS),
description: props.status.hasHTTPS ? 'Encrypted' : 'HTTP only', description: props.status.hasHTTPS ? 'Encrypted' : 'HTTP only',
critical: true,
}, },
{ {
key: 'audit', key: 'audit',
label: 'Audit log', label: 'Audit log',
enabled: props.status.hasAuditLogging, enabled: props.status.hasAuditLogging,
description: props.status.hasAuditLogging ? 'Active' : 'Not enabled', description: props.status.hasAuditLogging ? 'Active' : 'Not enabled',
critical: false,
}, },
]; ];
const badgeClasses = (enabled: boolean) => // Calculate security score
enabled const securityScore = createMemo(() => {
? 'inline-flex items-center gap-1 px-2.5 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300' const list = items();
: 'inline-flex items-center gap-1 px-2.5 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-200'; const criticalItems = list.filter(i => i.critical);
const enabledCritical = criticalItems.filter(i => i.enabled).length;
const allItems = list.filter(i => i.enabled).length;
// Weight critical items more heavily
const criticalWeight = 0.7;
const optionalWeight = 0.3;
const criticalScore = criticalItems.length > 0 ? (enabledCritical / criticalItems.length) * criticalWeight : 0;
const optionalScore = list.length > 0 ? (allItems / list.length) * optionalWeight : 0;
return Math.round((criticalScore + optionalScore) * 100);
});
const scoreColor = () => {
const score = securityScore();
if (score >= 80) return 'from-green-500 to-emerald-600';
if (score >= 50) return 'from-amber-500 to-orange-600';
return 'from-red-500 to-rose-600';
};
const scoreLabel = () => {
const score = securityScore();
if (score >= 80) return 'Strong';
if (score >= 50) return 'Moderate';
return 'Weak';
};
const ScoreIcon = () => {
const score = securityScore();
if (score >= 80) return ShieldCheck;
if (score >= 50) return Shield;
return ShieldAlert;
};
return ( return (
<Card padding="md" class="border border-gray-200 dark:border-gray-700"> <Card
<div class="flex flex-col gap-4"> padding="none"
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-3"> class="overflow-hidden border border-gray-200 dark:border-gray-700"
<SectionHeader title="Security posture" size="sm" class="flex-1" /> border={false}
<div class="flex items-center gap-2"> >
<span {/* Header with Security Score */}
class={`${ <div class={`bg-gradient-to-r ${scoreColor()} px-6 py-5`}>
props.status.publicAccess && !props.status.isPrivateNetwork <div class="flex items-center justify-between">
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' <div class="flex items-center gap-4">
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-200' <div class="p-3 bg-white/20 rounded-xl backdrop-blur-sm">
} px-3 py-1 text-xs font-semibold rounded-full`} {(() => {
> const Icon = ScoreIcon();
{props.status.publicAccess && !props.status.isPrivateNetwork return <Icon class="w-6 h-6 text-white" />;
? 'Public network access' })()}
: 'Private network access'} </div>
</span> <div>
<Show when={props.status.clientIP}> <h2 class="text-lg font-bold text-white">Security Posture</h2>
<span class="hidden md:inline-flex items-center px-3 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300"> <p class="text-sm text-white/80">
IP: {props.status.clientIP} {props.status.publicAccess && !props.status.isPrivateNetwork
</span> ? 'Public network access detected'
</Show> : 'Private network access'}
</p>
</div>
</div>
<div class="text-right">
<div class="text-3xl font-bold text-white">{securityScore()}%</div>
<div class="text-sm text-white/80">{scoreLabel()}</div>
</div> </div>
</div> </div>
</div>
{/* Security Items Grid */}
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3"> <div class="p-6">
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<For each={items()}> <For each={items()}>
{(item) => ( {(item) => (
<div class="rounded-lg border border-gray-200 dark:border-gray-700 p-3 bg-white dark:bg-gray-800"> <div class={`rounded-xl border p-4 transition-all ${item.enabled
? 'border-green-200 dark:border-green-800 bg-green-50/50 dark:bg-green-900/20'
: item.critical
? 'border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-900/20'
: 'border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-800/50'
}`}>
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{item.label} {item.label}
</span> </span>
<span class={badgeClasses(item.enabled)}> <Show when={item.enabled} fallback={
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <XCircle class={`w-5 h-5 ${item.critical ? 'text-red-500' : 'text-gray-400 dark:text-gray-500'}`} />
<path }>
stroke-linecap="round" <CheckCircle class="w-5 h-5 text-green-500" />
stroke-linejoin="round" </Show>
stroke-width="2" </div>
d={item.enabled ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12'} <div class="flex items-center justify-between">
/> <p class="text-xs text-gray-600 dark:text-gray-400">
</svg> {item.description}
{item.enabled ? 'On' : 'Off'} </p>
</span> <Show when={item.critical && !item.enabled}>
<span class="text-[10px] font-medium text-red-600 dark:text-red-400 uppercase">
Critical
</span>
</Show>
</div> </div>
<p class="text-xs text-gray-600 dark:text-gray-400 leading-relaxed">
{item.description}
</p>
</div> </div>
)} )}
</For> </For>
</div> </div>
{/* Client IP Badge */}
<Show when={props.status.clientIP}>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-end">
<span class="inline-flex items-center px-3 py-1.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300">
Your IP: {props.status.clientIP}
</span>
</div>
</Show>
</div> </div>
</Card> </Card>
); );