chore: clean .gitignore cache
This commit is contained in:
@@ -1,339 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Plus, Pencil, Trash2, Shield, Copy, Eye, Info } from 'lucide-react';
|
||||
import {
|
||||
useSecurityHeaderProfiles,
|
||||
useCreateSecurityHeaderProfile,
|
||||
useUpdateSecurityHeaderProfile,
|
||||
useDeleteSecurityHeaderProfile,
|
||||
} from '../hooks/useSecurityHeaders';
|
||||
import { SecurityHeaderProfileForm } from '../components/SecurityHeaderProfileForm';
|
||||
import { SecurityScoreDisplay } from '../components/SecurityScoreDisplay';
|
||||
import type { SecurityHeaderProfile, CreateProfileRequest } from '../api/securityHeaders';
|
||||
import { createBackup } from '../api/backups';
|
||||
import toast from 'react-hot-toast';
|
||||
import { PageShell } from '../components/layout/PageShell';
|
||||
import {
|
||||
Button,
|
||||
Alert,
|
||||
Card,
|
||||
EmptyState,
|
||||
SkeletonTable,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
Tooltip,
|
||||
TooltipTrigger,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
} from '../components/ui';
|
||||
|
||||
export default function SecurityHeaders() {
|
||||
const { t } = useTranslation();
|
||||
const { data: profiles, isLoading } = useSecurityHeaderProfiles();
|
||||
const createMutation = useCreateSecurityHeaderProfile();
|
||||
const updateMutation = useUpdateSecurityHeaderProfile();
|
||||
const deleteMutation = useDeleteSecurityHeaderProfile();
|
||||
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
const [editingProfile, setEditingProfile] = useState<SecurityHeaderProfile | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<SecurityHeaderProfile | null>(null);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const handleCreate = (data: CreateProfileRequest) => {
|
||||
createMutation.mutate(data, {
|
||||
onSuccess: () => setShowCreateForm(false),
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdate = (data: CreateProfileRequest) => {
|
||||
if (!editingProfile) return;
|
||||
updateMutation.mutate(
|
||||
{ id: editingProfile.id, data },
|
||||
{
|
||||
onSuccess: () => setEditingProfile(null),
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const handleDeleteWithBackup = async (profile: SecurityHeaderProfile) => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
toast.loading(t('securityHeaders.creatingBackup'), { id: 'backup-toast' });
|
||||
await createBackup();
|
||||
toast.success(t('securityHeaders.backupCreated'), { id: 'backup-toast' });
|
||||
|
||||
deleteMutation.mutate(profile.id, {
|
||||
onSuccess: () => {
|
||||
setShowDeleteConfirm(null);
|
||||
setEditingProfile(null);
|
||||
toast.success(t('securityHeaders.deleteSuccess', { name: profile.name }));
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(t('securityHeaders.deleteFailed', { error: error.message }));
|
||||
},
|
||||
onSettled: () => {
|
||||
setIsDeleting(false);
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
toast.error(t('securityHeaders.backupFailed'), { id: 'backup-toast' });
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloneProfile = (profile: SecurityHeaderProfile) => {
|
||||
const clonedData: CreateProfileRequest = {
|
||||
name: `${profile.name} (Copy)`,
|
||||
description: profile.description,
|
||||
hsts_enabled: profile.hsts_enabled,
|
||||
hsts_max_age: profile.hsts_max_age,
|
||||
hsts_include_subdomains: profile.hsts_include_subdomains,
|
||||
hsts_preload: profile.hsts_preload,
|
||||
csp_enabled: profile.csp_enabled,
|
||||
csp_directives: profile.csp_directives,
|
||||
csp_report_only: profile.csp_report_only,
|
||||
csp_report_uri: profile.csp_report_uri,
|
||||
x_frame_options: profile.x_frame_options,
|
||||
x_content_type_options: profile.x_content_type_options,
|
||||
referrer_policy: profile.referrer_policy,
|
||||
permissions_policy: profile.permissions_policy,
|
||||
cross_origin_opener_policy: profile.cross_origin_opener_policy,
|
||||
cross_origin_resource_policy: profile.cross_origin_resource_policy,
|
||||
cross_origin_embedder_policy: profile.cross_origin_embedder_policy,
|
||||
xss_protection: profile.xss_protection,
|
||||
cache_control_no_store: profile.cache_control_no_store,
|
||||
};
|
||||
|
||||
createMutation.mutate(clonedData);
|
||||
};
|
||||
|
||||
const customProfiles = profiles?.filter((p: SecurityHeaderProfile) => !p.is_preset) || [];
|
||||
const presetProfiles = (profiles?.filter((p: SecurityHeaderProfile) => p.is_preset) || [])
|
||||
.sort((a, b) => a.security_score - b.security_score);
|
||||
|
||||
// Get tooltip content for preset types
|
||||
const getPresetTooltip = (presetType: string): string => {
|
||||
switch (presetType) {
|
||||
case 'basic':
|
||||
return t('securityHeaders.presets.basic');
|
||||
case 'api-friendly':
|
||||
return t('securityHeaders.presets.apiFriendly');
|
||||
case 'strict':
|
||||
return t('securityHeaders.presets.strict');
|
||||
case 'paranoid':
|
||||
return t('securityHeaders.presets.paranoid');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PageShell
|
||||
title={t('securityHeaders.title')}
|
||||
description={t('securityHeaders.description')}
|
||||
actions={
|
||||
<Button onClick={() => setShowCreateForm(true)}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
{t('securityHeaders.createProfile')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{/* Info Alert */}
|
||||
<Alert variant="info" className="mb-6">
|
||||
<Shield className="w-4 h-4" />
|
||||
<div>
|
||||
<p className="font-semibold">{t('securityHeaders.alertTitle')}</p>
|
||||
<p className="text-sm mt-1">
|
||||
{t('securityHeaders.alertDescription')}
|
||||
</p>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
{/* Quick Presets (Read-Only) */}
|
||||
{presetProfiles.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||
{t('securityHeaders.systemProfiles')}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{t('securityHeaders.systemProfilesDescription')}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<TooltipProvider>
|
||||
{presetProfiles.map((profile: SecurityHeaderProfile) => (
|
||||
<Card key={profile.id} className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">{profile.name}</h3>
|
||||
{profile.preset_type && getPresetTooltip(profile.preset_type) && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button" className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<Info className="w-4 h-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs whitespace-pre-line text-left">
|
||||
{getPresetTooltip(profile.preset_type)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SecurityScoreDisplay
|
||||
score={profile.security_score}
|
||||
size="sm"
|
||||
showDetails={false}
|
||||
/>
|
||||
</div>
|
||||
{profile.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{profile.description}</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setEditingProfile(profile)}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" /> {t('securityHeaders.view')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleCloneProfile(profile)}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-1" /> {t('securityHeaders.clone')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Profiles Section */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">{t('securityHeaders.customProfiles')}</h2>
|
||||
|
||||
{isLoading ? (
|
||||
<SkeletonTable rows={3} />
|
||||
) : customProfiles.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={<Shield className="w-12 h-12" />}
|
||||
title={t('securityHeaders.noCustomProfiles')}
|
||||
description={t('securityHeaders.noCustomProfilesDescription')}
|
||||
action={{
|
||||
label: t('securityHeaders.createProfile'),
|
||||
onClick: () => setShowCreateForm(true),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{customProfiles.map((profile: SecurityHeaderProfile) => (
|
||||
<Card key={profile.id} className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">{profile.name}</h3>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('securityHeaders.updated')} {new Date(profile.updated_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<SecurityScoreDisplay
|
||||
score={profile.security_score}
|
||||
size="sm"
|
||||
showDetails={false}
|
||||
/>
|
||||
</div>
|
||||
{profile.description && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">{profile.description}</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setEditingProfile(profile)}
|
||||
className="flex-1"
|
||||
>
|
||||
<Pencil className="w-3 h-3 mr-1" />
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleCloneProfile(profile)}
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowDeleteConfirm(profile)}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create/Edit Dialog */}
|
||||
<Dialog open={showCreateForm || editingProfile !== null} onOpenChange={(open: boolean) => {
|
||||
if (!open) {
|
||||
setShowCreateForm(false);
|
||||
setEditingProfile(null);
|
||||
}
|
||||
}}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingProfile
|
||||
? (editingProfile.is_preset ? t('securityHeaders.viewProfile') : t('securityHeaders.editProfile'))
|
||||
: t('securityHeaders.createProfileTitle')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<SecurityHeaderProfileForm
|
||||
initialData={editingProfile || undefined}
|
||||
onSubmit={editingProfile ? handleUpdate : handleCreate}
|
||||
onCancel={() => {
|
||||
setShowCreateForm(false);
|
||||
setEditingProfile(null);
|
||||
}}
|
||||
onDelete={editingProfile && !editingProfile.is_preset ? () => setShowDeleteConfirm(editingProfile) : undefined}
|
||||
isLoading={createMutation.isPending || updateMutation.isPending}
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={showDeleteConfirm !== null} onOpenChange={(open: boolean) => !open && setShowDeleteConfirm(null)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('securityHeaders.confirmDeletion')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{t('securityHeaders.deleteConfirmMessage', { name: showDeleteConfirm?.name })}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowDeleteConfirm(null)} disabled={isDeleting}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => showDeleteConfirm && handleDeleteWithBackup(showDeleteConfirm)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? t('securityHeaders.deleting') : t('common.delete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user