Add firewall rules logic for all hosts

This commit is contained in:
2026-09-27 14:25:27 +01:00
parent bbf29a5c1a
commit dbae73a746
13 changed files with 1115 additions and 15 deletions
@@ -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>
);
}
+2
View File
@@ -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>
</>
);
+43
View File
@@ -0,0 +1,43 @@
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
import userAuth from "@/src/functions/backend/auth/user-auth";
import grabFirewallStatus, {
type FirewallStatus,
} from "@/src/functions/backend/firewall/grab-firewall-status";
import type {
APIResponseObject,
BunextAPIRouteHandler,
} from "@moduletrace/bunext/types";
export const handler: BunextAPIRouteHandler<
APIResponseObject<FirewallStatus>
> = async ({ req }) => {
if (req.method !== "GET") {
return {
success: false,
};
}
const { user, user_types } = await userAuth({ req });
if (!user?.logged_in_status) {
return {
success: false,
msg: `Unauthorized`,
logoutUser: true,
};
}
const is_super_admin = checkUserAccess({ user_types });
if (!is_super_admin.success) {
return {
success: false,
msg: `Unauthorized`,
};
}
return {
success: true,
singleRes: await grabFirewallStatus(),
};
};