Add firewall rules logic for all hosts
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import type { FirewallStatus } from "@/src/functions/backend/firewall/grab-firewall-status";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type FirewallStatusResponse = {
|
||||
success: boolean;
|
||||
singleRes?: FirewallStatus | null;
|
||||
msg?: string;
|
||||
};
|
||||
|
||||
export default function useFirewallStatus() {
|
||||
const [firewall_status, setFirewallStatus] =
|
||||
useState<FirewallStatus | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = () =>
|
||||
fetchApi<ApiReqParams, FirewallStatusResponse>(
|
||||
`/api/admin/firewall-status`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
).then((res) => {
|
||||
if (res.success && res.singleRes) {
|
||||
setFirewallStatus(res.singleRes);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(res.msg || `Could not fetch firewall status`);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
const refreshWithLoading = () => {
|
||||
setLoading(true);
|
||||
|
||||
refresh()
|
||||
.catch((request_error: any) => {
|
||||
setError(
|
||||
request_error?.message ||
|
||||
`Could not fetch firewall status`,
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshWithLoading();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
firewall_status,
|
||||
refresh: refreshWithLoading,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import CodeBlock from "@/src/components/twui/elements/CodeBlock";
|
||||
import Tag from "@/src/components/twui/elements/Tag";
|
||||
import type {
|
||||
FirewallRuleKind,
|
||||
FirewallRuleInfo,
|
||||
} from "@/src/functions/backend/firewall/grab-firewall-status";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import type { TWUIButtonColorType } from "@/src/components/twui/layout/Button";
|
||||
import {
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
ShieldX,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
const KIND_META: Record<
|
||||
FirewallRuleKind,
|
||||
{
|
||||
label: string;
|
||||
color: TWUIButtonColorType;
|
||||
icon: LucideIcon;
|
||||
text_class: string;
|
||||
}
|
||||
> = {
|
||||
required: {
|
||||
label: `Active`,
|
||||
color: `success`,
|
||||
icon: ShieldCheck,
|
||||
text_class: `text-success`,
|
||||
},
|
||||
temporary: {
|
||||
label: `Temporary`,
|
||||
color: `warning`,
|
||||
icon: ShieldAlert,
|
||||
text_class: `text-warning`,
|
||||
},
|
||||
orphaned: {
|
||||
label: `Orphaned`,
|
||||
color: `error`,
|
||||
icon: ShieldX,
|
||||
text_class: `text-error`,
|
||||
},
|
||||
other: {
|
||||
label: `Unmanaged`,
|
||||
color: `gray`,
|
||||
icon: ShieldCheck,
|
||||
text_class: `text-foreground-light/45 dark:text-foreground-dark/45`,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
rule: FirewallRuleInfo;
|
||||
};
|
||||
|
||||
export default function FirewallRuleRow({ rule }: Props) {
|
||||
const meta = KIND_META[rule.kind];
|
||||
const Icon = meta.icon;
|
||||
|
||||
return (
|
||||
<Stack className="w-full items-stretch gap-2.5 rounded-lg bg-background-light dark:bg-[#0e0e10] border border-slate-200 dark:border-white/10 p-3">
|
||||
<Row className="w-full items-start justify-between gap-3 flex-wrap">
|
||||
<Row className="gap-2.5 items-start min-w-0">
|
||||
<Icon
|
||||
size={15}
|
||||
className={`mt-0.5 shrink-0 ${meta.text_class}`}
|
||||
/>
|
||||
<Span className="text-[12.5px] leading-5 min-w-0">
|
||||
{rule.description}
|
||||
</Span>
|
||||
</Row>
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color={meta.color}
|
||||
className="shrink-0"
|
||||
>
|
||||
{meta.label}
|
||||
</Tag>
|
||||
</Row>
|
||||
|
||||
{rule.delete_command ? (
|
||||
<CodeBlock
|
||||
singleBlock
|
||||
language="shell"
|
||||
wrapperProps={{
|
||||
className: `max-w-none! xl:max-w-none!`,
|
||||
}}
|
||||
>
|
||||
{`# remove rule\n${rule.delete_command}`}
|
||||
</CodeBlock>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import AdminCard from "@/src/components/general/admin-card";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import H3 from "@/src/components/twui/layout/H3";
|
||||
import P from "@/src/components/twui/layout/P";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import Tag from "@/src/components/twui/elements/Tag";
|
||||
import { RefreshCw, Shield, TriangleAlert } from "lucide-react";
|
||||
import useFirewallStatus from "../(hooks)/use-firewall-status";
|
||||
import FirewallRuleRow from "../(partials)/firewall-rule-row";
|
||||
|
||||
const MANAGER_LABEL: Record<string, string> = {
|
||||
firewalld: `firewalld (active)`,
|
||||
ufw: `ufw (active)`,
|
||||
none: `none detected`,
|
||||
};
|
||||
|
||||
export default function FirewallSection() {
|
||||
const { firewall_status, refresh, loading, error } = useFirewallStatus();
|
||||
|
||||
if (!firewall_status) {
|
||||
return (
|
||||
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||
<Row className="gap-3 items-start">
|
||||
<Span className="p-2.5 rounded-lg bg-primary/10 dark:bg-primary-dark/10 text-primary dark:text-primary-dark shrink-0">
|
||||
<Shield size={19} />
|
||||
</Span>
|
||||
<Stack className="gap-1 min-w-0">
|
||||
<H3 className="text-[14px] font-semibold mb-0!">
|
||||
Firewall
|
||||
</H3>
|
||||
<P
|
||||
noMargin
|
||||
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||
>
|
||||
{error ||
|
||||
`Checking firewall status on this host ...`}
|
||||
</P>
|
||||
</Stack>
|
||||
</Row>
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
|
||||
const { manager, rules, hosts_found } = firewall_status;
|
||||
|
||||
return (
|
||||
<AdminCard className="w-full p-5 flex flex-col gap-4">
|
||||
<Row className="gap-3 items-start">
|
||||
<Span className="p-2.5 rounded-lg bg-primary/10 dark:bg-primary-dark/10 text-primary dark:text-primary-dark shrink-0">
|
||||
<Shield size={19} />
|
||||
</Span>
|
||||
<Stack className="gap-1 min-w-0 flex-1">
|
||||
<Row className="gap-2 items-center flex-wrap">
|
||||
<H3 className="text-[14px] font-semibold mb-0!">
|
||||
Firewall
|
||||
</H3>
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color={manager == "none" ? "warning" : "success"}
|
||||
className="shrink-0"
|
||||
>
|
||||
{MANAGER_LABEL[manager] || manager}
|
||||
</Tag>
|
||||
</Row>
|
||||
<P
|
||||
noMargin
|
||||
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
|
||||
>
|
||||
Host setups automatically allow their wireguard subnet
|
||||
and listen port here. Rules that are no longer needed
|
||||
(bootstrap or stale) are listed with removal commands.
|
||||
</P>
|
||||
</Stack>
|
||||
<Button
|
||||
title="Refresh"
|
||||
variant="outlined"
|
||||
loading={loading}
|
||||
beforeIcon={<RefreshCw size={14} />}
|
||||
onClick={refresh}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Row>
|
||||
|
||||
{manager == "none" ? (
|
||||
<Row className="gap-2 items-center bg-warning/5 rounded-lg px-3 py-2.5 text-warning">
|
||||
<TriangleAlert size={15} className="shrink-0" />
|
||||
<Span className="text-[12.5px]">
|
||||
No active high-level firewall manager detected
|
||||
(firewalld/ufw). If you use iptables/nftables directly,
|
||||
allow each host subnet and its wireguard listen port
|
||||
manually.
|
||||
</Span>
|
||||
</Row>
|
||||
) : null}
|
||||
|
||||
{rules.length ? (
|
||||
<Stack className="w-full gap-2 items-stretch">
|
||||
{rules.map((rule, index) => (
|
||||
<FirewallRuleRow
|
||||
key={rule.id || `${manager}-${index}`}
|
||||
rule={rule}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack className="w-full items-start gap-1 rounded-lg bg-background-light dark:bg-[#0e0e10] border border-slate-200 dark:border-white/10 p-3">
|
||||
<Span className="text-[12.5px] text-foreground-light/60 dark:text-foreground-dark/60">
|
||||
{hosts_found
|
||||
? `No wg-ui related firewall rules found.`
|
||||
: `No hosts created yet — creating a host automatically allows its subnet (e.g. 10.5.0.0/24) and its wireguard listen port (e.g. 51821/udp) here.`}
|
||||
</Span>
|
||||
</Stack>
|
||||
)}
|
||||
</AdminCard>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import Stack from "@/src/components/twui/layout/Stack";
|
||||
import useSystemSetupStatus from "./(hooks)/use-system-setup-status";
|
||||
import SystemStatusSection from "./(sections)/system-status-section";
|
||||
import SetupActionsSection from "./(sections)/setup-actions-section";
|
||||
import FirewallSection from "./(sections)/firewall-section";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
|
||||
@@ -26,6 +27,7 @@ export default function AdminWireguardSetupPage() {
|
||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||
<SystemStatusSection setup={setup} />
|
||||
<SetupActionsSection setup={setup} />
|
||||
<FirewallSection />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user