feat(crowdsec): IDS ein-/ausschalten via Switch — v1.2.63
- system.go: ServiceToggle-Endpoint (POST /system/service-toggle) start/stop + enable/disable für crowdsec + crowdsec-firewall-bouncer - system.go: crowdsec + crowdsec-firewall-bouncer in servicesToCheck - postinst: sudoers-Einträge für systemctl start/stop/enable/disable beider CrowdSec-Units - UI: Switch im StatusStrip für Agent + Bouncer, getrennt schaltbar, disabled wenn CrowdSec nicht installiert oder Viewer-Rolle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Tabs,
|
||||
Table,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
|
||||
import apiClient, { isEnvelope } from '../../api/client'
|
||||
import PageHeader from '../../components/PageHeader'
|
||||
@@ -73,27 +75,45 @@ async function fetchCollections(): Promise<HubItem[]> {
|
||||
return (r.data.data as { collections: HubItem[] }).collections ?? []
|
||||
}
|
||||
|
||||
async function toggleService(service: string, enabled: boolean): Promise<void> {
|
||||
await apiClient.post('/system/service-toggle', { service, enabled })
|
||||
}
|
||||
|
||||
// ---------- Status strip ----------------------------------------------------
|
||||
|
||||
function StatusStrip({ status }: { status: CrowdSecStatus | undefined }) {
|
||||
function StatusStrip({ status, onToggle }: { status: CrowdSecStatus | undefined; onToggle: () => void }) {
|
||||
const { t } = useTranslation()
|
||||
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||
|
||||
const agentColor = status?.agent_running ? 'green' : 'red'
|
||||
const bouncerColor = status?.bouncer_running ? 'green' : 'red'
|
||||
const agentLabel = status?.agent_running ? t('cs.status.running') : t('cs.status.stopped')
|
||||
const bouncerLabel = status?.bouncer_running ? t('cs.status.running') : t('cs.status.stopped')
|
||||
const toggleAgent = useMutation({
|
||||
mutationFn: (enabled: boolean) => toggleService('crowdsec', enabled),
|
||||
onSuccess: onToggle,
|
||||
})
|
||||
const toggleBouncer = useMutation({
|
||||
mutationFn: (enabled: boolean) => toggleService('crowdsec-firewall-bouncer', enabled),
|
||||
onSuccess: onToggle,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="fw-kpi-strip">
|
||||
<div className="fw-kpi-card">
|
||||
<div className="fw-kpi-label">{t('cs.status.agent')}</div>
|
||||
<div className="fw-kpi-value">
|
||||
<Tag
|
||||
icon={status?.agent_running ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
color={agentColor}
|
||||
>
|
||||
{agentLabel}
|
||||
</Tag>
|
||||
<Space size={8}>
|
||||
<Tag
|
||||
icon={status?.agent_running ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
color={status?.agent_running ? 'green' : 'red'}
|
||||
>
|
||||
{status?.agent_running ? t('cs.status.running') : t('cs.status.stopped')}
|
||||
</Tag>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={status?.agent_running ?? false}
|
||||
disabled={isViewer || !status?.installed}
|
||||
loading={toggleAgent.isPending}
|
||||
onChange={(checked) => toggleAgent.mutate(checked)}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
{status?.version && (
|
||||
<div className="fw-kpi-sub">{status.version}</div>
|
||||
@@ -102,12 +122,21 @@ function StatusStrip({ status }: { status: CrowdSecStatus | undefined }) {
|
||||
<div className="fw-kpi-card">
|
||||
<div className="fw-kpi-label">{t('cs.status.bouncer')}</div>
|
||||
<div className="fw-kpi-value">
|
||||
<Tag
|
||||
icon={status?.bouncer_running ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
color={bouncerColor}
|
||||
>
|
||||
{bouncerLabel}
|
||||
</Tag>
|
||||
<Space size={8}>
|
||||
<Tag
|
||||
icon={status?.bouncer_running ? <CheckCircleOutlined /> : <CloseCircleOutlined />}
|
||||
color={status?.bouncer_running ? 'green' : 'red'}
|
||||
>
|
||||
{status?.bouncer_running ? t('cs.status.running') : t('cs.status.stopped')}
|
||||
</Tag>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={status?.bouncer_running ?? false}
|
||||
disabled={isViewer || !status?.installed}
|
||||
loading={toggleBouncer.isPending}
|
||||
onChange={(checked) => toggleBouncer.mutate(checked)}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fw-kpi-card">
|
||||
@@ -496,6 +525,7 @@ function CollectionsTab() {
|
||||
|
||||
export default function CrowdSecPage() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ['crowdsec', 'status'],
|
||||
@@ -503,6 +533,10 @@ export default function CrowdSecPage() {
|
||||
refetchInterval: 10_000,
|
||||
})
|
||||
|
||||
const invalidateStatus = () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['crowdsec', 'status'] })
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ key: 'decisions', label: t('cs.tabs.decisions'), children: <DecisionsTab /> },
|
||||
{ key: 'alerts', label: t('cs.tabs.alerts'), children: <AlertsTab /> },
|
||||
@@ -526,7 +560,7 @@ export default function CrowdSecPage() {
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
<StatusStrip status={status} />
|
||||
<StatusStrip status={status} onToggle={invalidateStatus} />
|
||||
<Tabs items={tabs} defaultActiveKey="decisions" type="card" />
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user