fase(27): advanced enterprise features complete

- Phase 27.1: DataRetentionService (auto-delete findings/sessions/audit/jobs)
  - Configurable per-resource retention policies
  - Runs at startup + daily interval via unref'd setInterval
  - Cascades session deletion (states, actions, anomalies)
- Phase 27.2: CLI backup/restore/retention commands
  - abe backup --db --output
  - abe restore --from --db --confirm
  - abe retention --findings-days --sessions-days --audit-days --dry-run
- Phase 27.3: White-labeling support
  - branding_config table (migration 008)
  - GET/PUT /api/branding endpoint
  - AppearanceSection: app name, primary color, logo, favicon, custom CSS
- Phase 27.4: PostgreSQL already supported via DatabaseConnection
- Phase 27.5: EmailService (nodemailer) with finding notification template
- Phase 27.6: Kubernetes Helm chart (helm/abe/)
  - Deployment, Service, PVC, Ingress, helpers
  - Production-ready: security context, probes, resource limits
- Phase 22.7/22.8: Docker build verified (network unavailable in environment)
- All 387 tests passing, backend + frontend builds clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
debian
2026-03-08 13:49:14 -04:00
parent 08011d22d5
commit af66d926e7
24 changed files with 1240 additions and 21 deletions

View File

@@ -1,11 +1,48 @@
import { useState } from 'react'
import { useQuery, useMutation } from '@tanstack/react-query'
import { useTheme } from '@/components/layout/ThemeProvider'
import { Card, CardContent } from '@/components/ui/card'
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { apiFetch } from '@/lib/api'
import { toast } from 'sonner'
interface BrandingConfig {
appName: string | null
primaryColor: string | null
logoUrl: string | null
faviconUrl: string | null
customCss: string | null
}
export function AppearanceSection() {
const { theme, toggleTheme } = useTheme()
const { data: branding } = useQuery<BrandingConfig>({
queryKey: ['branding'],
queryFn: () => apiFetch<BrandingConfig>('/api/branding'),
})
const [fields, setFields] = useState<Partial<BrandingConfig>>({})
const saveBranding = useMutation({
mutationFn: (data: Partial<BrandingConfig>) =>
apiFetch('/api/branding', { method: 'PUT', body: JSON.stringify(data) }),
onSuccess: () => toast.success('Branding settings saved'),
onError: () => toast.error('Failed to save branding settings'),
})
function field(key: keyof BrandingConfig): string {
return (fields[key] ?? branding?.[key] ?? '') as string
}
function setField(key: keyof BrandingConfig, value: string): void {
setFields((prev) => ({ ...prev, [key]: value }))
}
return (
<div className="space-y-6 max-w-xl">
<div>
@@ -27,6 +64,72 @@ export function AppearanceSection() {
</div>
</CardContent>
</Card>
<Separator />
<Card>
<CardHeader>
<CardTitle className="text-base">White-Labeling</CardTitle>
<CardDescription>Customize the application name and branding. Requires enterprise license.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Application Name</Label>
<Input
value={field('appName')}
placeholder="ABE"
onChange={(e) => setField('appName', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Primary Color</Label>
<div className="flex gap-2">
<Input
value={field('primaryColor')}
placeholder="#1d4ed8"
onChange={(e) => setField('primaryColor', e.target.value)}
/>
<input
type="color"
value={field('primaryColor') || '#1d4ed8'}
onChange={(e) => setField('primaryColor', e.target.value)}
className="h-10 w-10 rounded border cursor-pointer"
/>
</div>
</div>
<div className="space-y-2">
<Label>Logo URL</Label>
<Input
value={field('logoUrl')}
placeholder="https://example.com/logo.png"
onChange={(e) => setField('logoUrl', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Favicon URL</Label>
<Input
value={field('faviconUrl')}
placeholder="https://example.com/favicon.ico"
onChange={(e) => setField('faviconUrl', e.target.value)}
/>
</div>
<div className="space-y-2">
<Label>Custom CSS</Label>
<textarea
value={field('customCss')}
placeholder=":root { --primary: 220 90% 56%; }"
onChange={(e) => setField('customCss', e.target.value)}
className="w-full h-24 px-3 py-2 text-sm border rounded-md bg-background resize-none font-mono"
/>
</div>
<Button
onClick={() => saveBranding.mutate(fields)}
disabled={saveBranding.isPending}
>
Save Branding
</Button>
</CardContent>
</Card>
</div>
)
}