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 { 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 {
status: {
@ -26,24 +30,28 @@ export const SecurityPostureSummary: Component<SecurityPostureSummaryProps> = (p
label: 'Password login',
enabled: props.status.hasAuthentication,
description: props.status.hasAuthentication ? 'Active' : 'Not configured',
critical: true, // Critical security feature
},
{
key: 'oidc',
label: 'Single sign-on',
enabled: Boolean(props.status.oidcEnabled),
description: props.status.oidcEnabled ? 'OIDC configured' : 'Not configured',
critical: false,
},
{
key: 'proxy',
label: 'Proxy auth',
enabled: Boolean(props.status.hasProxyAuth),
description: props.status.hasProxyAuth ? 'Active' : 'Not configured',
critical: false,
},
{
key: 'token',
label: 'API token',
enabled: props.status.apiTokenConfigured,
description: props.status.apiTokenConfigured ? 'Active' : 'Not configured',
critical: false,
},
{
key: 'export',
@ -52,79 +60,138 @@ export const SecurityPostureSummary: Component<SecurityPostureSummaryProps> = (p
description: props.status.unprotectedExportAllowed
? 'Unprotected'
: 'Token + passphrase required',
critical: true,
},
{
key: 'https',
label: 'HTTPS',
enabled: Boolean(props.status.hasHTTPS),
description: props.status.hasHTTPS ? 'Encrypted' : 'HTTP only',
critical: true,
},
{
key: 'audit',
label: 'Audit log',
enabled: props.status.hasAuditLogging,
description: props.status.hasAuditLogging ? 'Active' : 'Not enabled',
critical: false,
},
];
const badgeClasses = (enabled: boolean) =>
enabled
? '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'
: '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';
// Calculate security score
const securityScore = createMemo(() => {
const list = items();
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 (
<Card padding="md" class="border border-gray-200 dark:border-gray-700">
<div class="flex flex-col gap-4">
<div class="flex flex-col md:flex-row md:items-start md:justify-between gap-3">
<SectionHeader title="Security posture" size="sm" class="flex-1" />
<div class="flex items-center gap-2">
<span
class={`${
props.status.publicAccess && !props.status.isPrivateNetwork
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-200'
} px-3 py-1 text-xs font-semibold rounded-full`}
>
{props.status.publicAccess && !props.status.isPrivateNetwork
? 'Public network access'
: 'Private network access'}
</span>
<Show when={props.status.clientIP}>
<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">
IP: {props.status.clientIP}
</span>
</Show>
<Card
padding="none"
class="overflow-hidden border border-gray-200 dark:border-gray-700"
border={false}
>
{/* Header with Security Score */}
<div class={`bg-gradient-to-r ${scoreColor()} px-6 py-5`}>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="p-3 bg-white/20 rounded-xl backdrop-blur-sm">
{(() => {
const Icon = ScoreIcon();
return <Icon class="w-6 h-6 text-white" />;
})()}
</div>
<div>
<h2 class="text-lg font-bold text-white">Security Posture</h2>
<p class="text-sm text-white/80">
{props.status.publicAccess && !props.status.isPrivateNetwork
? 'Public network access detected'
: '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 class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{/* Security Items Grid */}
<div class="p-6">
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<For each={items()}>
{(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">
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
{item.label}
</span>
<span class={badgeClasses(item.enabled)}>
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d={item.enabled ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12'}
/>
</svg>
{item.enabled ? 'On' : 'Off'}
</span>
<Show when={item.enabled} fallback={
<XCircle class={`w-5 h-5 ${item.critical ? 'text-red-500' : 'text-gray-400 dark:text-gray-500'}`} />
}>
<CheckCircle class="w-5 h-5 text-green-500" />
</Show>
</div>
<div class="flex items-center justify-between">
<p class="text-xs text-gray-600 dark:text-gray-400">
{item.description}
</p>
<Show when={item.critical && !item.enabled}>
<span class="text-[10px] font-medium text-red-600 dark:text-red-400 uppercase">
Critical
</span>
</Show>
</div>
<p class="text-xs text-gray-600 dark:text-gray-400 leading-relaxed">
{item.description}
</p>
</div>
)}
</For>
</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>
</Card>
);