This commit is contained in:
2026-09-20 07:27:29 +01:00
parent dc2cd66b25
commit 8ec44ee0a7
70 changed files with 2657 additions and 128 deletions
@@ -0,0 +1,105 @@
import { useState, type Dispatch, type SetStateAction } from "react";
import { Plus } from "lucide-react";
import Button from "@/src/components/twui/layout/Button";
import Stack from "@/src/components/twui/layout/Stack";
import expandClientRuleDestinations from "@/src/functions/backend/setup/expand-client-rule-destinations";
import sanitizeClientRule from "@/src/functions/backend/setup/sanitize-client-rule";
import validateClientRule from "@/src/functions/backend/setup/validate-client-rule";
import ClientRuleFields from "./client-rule-fields";
import {
clientRuleDraftToRecord,
clientRuleToDraft,
createClientRuleDraft,
type ClientRuleDraft,
} from "./client-rule-draft";
import Modal from "@/src/components/twui/elements/Modal";
import useStatus from "@/src/components/twui/hooks/useStatus";
import H4 from "@/src/components/twui/layout/H4";
import Span from "@/src/components/twui/layout/Span";
import Form from "@/src/components/twui/form/Form";
import Divider from "@/src/components/twui/layout/Divider";
type Props = {
rules: ClientRuleDraft[];
setRules: (rules: ClientRuleDraft[]) => void;
setError: Dispatch<SetStateAction<string | undefined>>;
};
export default function AddClientRuleModal({
rules,
setRules,
setError,
}: Props) {
const [draft, setDraft] = useState<ClientRuleDraft>(
createClientRuleDraft(),
);
const { open, setOpen } = useStatus();
function handleAdd() {
const sanitized = sanitizeClientRule({
rule: clientRuleDraftToRecord({ draft }),
});
const valid = validateClientRule({ rule: sanitized });
if (!valid.success || !sanitized) {
setError(valid.msg || "Invalid rule");
return;
}
const expanded = expandClientRuleDestinations({ rule: sanitized }).map(
(rule) => clientRuleToDraft({ rule }),
);
setRules([...rules, ...expanded]);
setDraft(createClientRuleDraft());
setError(undefined);
}
const is_ll_access = Boolean(rules.find((r) => r.rule_type == "all"));
if (is_ll_access) {
return null;
}
return (
<Modal
open={open}
setOpen={setOpen}
target={
<Button
title="Add Client Rule"
size="smaller"
variant="outlined"
>
Add Client Rule
</Button>
}
>
<Stack className="w-full items-stretch gap-8">
<Stack className="gap-1!">
<H4>Add Rule</H4>
<Span>Add a new rule for this client</Span>{" "}
</Stack>
<Divider />
<ClientRuleFields draft={draft} setDraft={setDraft} />
<Button
title="Add access rule"
type="button"
variant="outlined"
size="small"
beforeIcon={<Plus size={14} />}
onClick={(e) => {
e.preventDefault();
handleAdd();
setOpen(false);
}}
>
Add rule
</Button>
</Stack>
</Modal>
);
}
@@ -0,0 +1,29 @@
import Input from "@/src/components/twui/form/Input";
import type { ClientRuleDraft } from "./client-rule-draft";
type Props = {
draft: ClientRuleDraft;
setDraft: (draft: ClientRuleDraft) => void;
};
export default function ClientRuleDestinationField({ draft, setDraft }: Props) {
if (draft.rule_type == "all") {
return null;
}
return (
<Input
title="Destination IP"
placeholder="10.0.0.5 or 192.168.1.0/24"
showLabel
defaultValue={draft.destination}
changeHandler={(value) => {
setDraft({
...draft,
destination: value,
});
}}
info="One IPv4 or CIDR. Comma-separate to add several."
/>
);
}
@@ -0,0 +1,42 @@
import { ShieldCheck, Trash2 } from "lucide-react";
import Button from "@/src/components/twui/layout/Button";
import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
import formatClientRuleLabel from "@/src/functions/frontend/format-client-rule-label";
import {
clientRuleDraftToRecord,
type ClientRuleDraft,
} from "./client-rule-draft";
type Props = {
draft: ClientRuleDraft;
onRemove: ({ local_id }: { local_id: string }) => void;
};
export default function ClientRuleDraftItem({ draft, onRemove }: Props) {
return (
<Row className="w-full justify-between gap-3 py-2.5 border-t border-slate-200/60 dark:border-white/5">
<Row className="items-center gap-2!">
<ShieldCheck size={15} className="-mt-[4px] text-success" />
<Span className="font-mono text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70 break-all">
{formatClientRuleLabel({
rule: clientRuleDraftToRecord({ draft }),
})}
</Span>
</Row>
<Button
title="Remove rule"
variant="ghost"
color="error"
size="smaller"
beforeIcon={<Trash2 size={14} />}
type="button"
onClick={() => {
onRemove({ local_id: draft.local_id });
}}
>
Remove
</Button>
</Row>
);
}
@@ -0,0 +1,56 @@
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db";
import type { ClientRuleProtocol, ClientRuleType } from "@/src/types";
export type ClientRuleDraft = {
local_id: string;
id?: number | "";
rule_type: ClientRuleType;
destination?: string;
ports?: string;
protocol: ClientRuleProtocol;
};
type CreateParams = Partial<ClientRuleDraft>;
export function createClientRuleDraft(params?: CreateParams): ClientRuleDraft {
return {
local_id: crypto.randomUUID(),
rule_type: "destination",
destination: "",
ports: "",
protocol: "any",
...params,
};
}
export function clientRuleToDraft({
rule,
}: {
rule: BUN_SQLITE_WGUI_CLIENT_RULES;
}): ClientRuleDraft {
return {
local_id: String(rule.id || crypto.randomUUID()),
id: rule.id,
rule_type: rule.rule_type == "all" ? "all" : "destination",
destination: rule.destination || "",
ports: rule.ports || "",
protocol:
rule.protocol == "tcp" || rule.protocol == "udp"
? rule.protocol
: "any",
};
}
export function clientRuleDraftToRecord({
draft,
}: {
draft: ClientRuleDraft;
}): BUN_SQLITE_WGUI_CLIENT_RULES {
return {
...(draft.id ? { id: draft.id } : {}),
rule_type: draft.rule_type,
destination: draft.destination || "",
ports: draft.ports || "",
protocol: draft.protocol,
};
}
@@ -0,0 +1,25 @@
import Stack from "@/src/components/twui/layout/Stack";
import type { ClientRuleDraft } from "./client-rule-draft";
import ClientRuleDestinationField from "./client-rule-destination-field";
import ClientRulePortsField from "./client-rule-ports-field";
import ClientRuleProtocolField from "./client-rule-protocol-field";
import ClientRuleTypeField from "./client-rule-type-field";
import Form from "@/src/components/twui/form/Form";
type Props = {
draft: ClientRuleDraft;
setDraft: (draft: ClientRuleDraft) => void;
};
export default function ClientRuleFields({ draft, setDraft }: Props) {
return (
<Form>
<Stack className="w-full items-stretch gap-4">
<ClientRuleTypeField draft={draft} setDraft={setDraft} />
<ClientRuleDestinationField draft={draft} setDraft={setDraft} />
<ClientRulePortsField draft={draft} setDraft={setDraft} />
<ClientRuleProtocolField draft={draft} setDraft={setDraft} />
</Stack>
</Form>
);
}
@@ -0,0 +1,29 @@
import Input from "@/src/components/twui/form/Input";
import type { ClientRuleDraft } from "./client-rule-draft";
type Props = {
draft: ClientRuleDraft;
setDraft: (draft: ClientRuleDraft) => void;
};
export default function ClientRulePortsField({ draft, setDraft }: Props) {
if (draft.rule_type == "all") {
return null;
}
return (
<Input
title="Ports"
placeholder="80,443 or 8000:8080"
showLabel
value={draft.ports}
onChange={(e) => {
setDraft({
...draft,
ports: e.target.value,
});
}}
info="Leave empty for any port."
/>
);
}
@@ -0,0 +1,33 @@
import Select from "@/src/components/twui/form/Select";
import { ClientRuleProtocols } from "@/src/dict/client-rules-dict";
import type { ClientRuleProtocol } from "@/src/types";
import type { ClientRuleDraft } from "./client-rule-draft";
type Props = {
draft: ClientRuleDraft;
setDraft: (draft: ClientRuleDraft) => void;
};
export default function ClientRuleProtocolField({ draft, setDraft }: Props) {
if (draft.rule_type == "all") {
return null;
}
return (
<Select<ClientRuleProtocol>
title="Protocol"
showLabel
options={ClientRuleProtocols.map((protocol) => ({
title: protocol.title,
value: protocol.value,
default: protocol.value == draft.protocol,
}))}
changeHandler={(value) => {
setDraft({
...draft,
protocol: value,
});
}}
/>
);
}
@@ -0,0 +1,45 @@
import Select from "@/src/components/twui/form/Select";
import { ClientRuleTypes } from "@/src/dict/client-rules-dict";
import type { ClientRuleType } from "@/src/types";
import type { ClientRuleDraft } from "./client-rule-draft";
import Checkbox from "@/src/components/twui/form/Checkbox";
type Props = {
draft: ClientRuleDraft;
setDraft: (draft: ClientRuleDraft) => void;
};
export default function ClientRuleTypeField({ draft, setDraft }: Props) {
return (
<Checkbox
defaultChecked={draft.rule_type == "all"}
label={`Allow all access?`}
info={`This allows unlimited access to all clients in this network`}
changeHandler={(checked) => {
setDraft({
...draft,
rule_type: checked ? "all" : "destination",
});
}}
wrapperWrapperProps={{ className: `mb-4` }}
/>
);
// return (
// <Select<ClientRuleType>
// title="Access"
// showLabel
// options={ClientRuleTypes.map((type) => ({
// title: type.title,
// value: type.value,
// default: type.value == draft.rule_type,
// }))}
// changeHandler={(value) => {
// setDraft({
// ...draft,
// rule_type: value,
// });
// }}
// />
// );
}
@@ -0,0 +1,76 @@
import { useState } from "react";
import { Plus } from "lucide-react";
import Button from "@/src/components/twui/layout/Button";
import H4 from "@/src/components/twui/layout/H4";
import P from "@/src/components/twui/layout/P";
import Stack from "@/src/components/twui/layout/Stack";
import Tag from "@/src/components/twui/elements/Tag";
import expandClientRuleDestinations from "@/src/functions/backend/setup/expand-client-rule-destinations";
import sanitizeClientRule from "@/src/functions/backend/setup/sanitize-client-rule";
import validateClientRule from "@/src/functions/backend/setup/validate-client-rule";
import ClientRuleDraftItem from "./client-rule-draft-item";
import ClientRulesEmpty from "./client-rules-empty";
import {
clientRuleDraftToRecord,
clientRuleToDraft,
createClientRuleDraft,
type ClientRuleDraft,
} from "./client-rule-draft";
import AddClientRuleModal from "./add-client-rule-modal";
import Row from "@/src/components/twui/layout/Row";
import Paper from "@/src/components/twui/elements/Paper";
type Props = {
rules: ClientRuleDraft[];
setRules: (rules: ClientRuleDraft[]) => void;
};
export default function ClientRulesEditor({ rules, setRules }: Props) {
const [error, setError] = useState<string>();
return (
<Paper className="w-full xl:p-6">
<Stack className="w-full items-stretch gap-4">
<Row className="w-full justify-between">
<Stack className="gap-1">
<H4 className="mb-0!">Access rules</H4>
<P
noMargin
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
>
Zero trust: nothing is allowed until you add a rule.
</P>
</Stack>
<AddClientRuleModal
rules={rules}
setRules={setRules}
setError={setError}
/>
</Row>
{error ? <Tag color="error">{error}</Tag> : null}
{rules[0] ? (
<Stack className="w-full items-stretch">
{rules.map((rule) => (
<ClientRuleDraftItem
key={rule.local_id}
draft={rule}
onRemove={({ local_id }) => {
setRules(
rules.filter(
(item) => item.local_id != local_id,
),
);
}}
/>
))}
</Stack>
) : (
<ClientRulesEmpty />
)}
</Stack>
</Paper>
);
}
@@ -0,0 +1,9 @@
import EmptyContent from "@/src/components/twui/elements/EmptyContent";
export default function ClientRulesEmpty() {
return (
<EmptyContent
title={`No rules. This client cannot reach the host, LAN, internet, or other clients.`}
/>
);
}
@@ -6,7 +6,8 @@ import Stack from "@/src/components/twui/layout/Stack";
import ClientRow from "@/src/components/general/client-row";
import { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
import { AppContext } from "@/src/pages/__root";
import { useContext } from "react";
import { useContext, useMemo } from "react";
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db";
type Props = {
host_id: string | number;
@@ -32,6 +33,33 @@ export default function ClientsList({ host_id }: Props) {
initial_res: pageProps?.clients || undefined,
});
const { res: client_rules } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENT_RULES>({
table: "client_rules",
sql_query: {
limit: 500,
},
initial_res: pageProps?.client_rules || undefined,
});
const rules_by_client_id = useMemo(() => {
const map = new Map<number, BUN_SQLITE_WGUI_CLIENT_RULES[]>();
for (let i = 0; i < (client_rules || []).length; i++) {
const rule = client_rules?.[i];
const client_id = Number(rule?.client_id);
if (!rule || !client_id) {
continue;
}
const existing = map.get(client_id) || [];
existing.push(rule);
map.set(client_id, existing);
}
return map;
}, [client_rules]);
if (!clients) {
return (
<Paper>
@@ -60,6 +88,7 @@ export default function ClientsList({ host_id }: Props) {
<th className={thClass}>Client</th>
<th className={thClass}>Tunnel IP</th>
<th className={thClass}>Allowed IPs</th>
<th className={thClass}>Access</th>
<th className={thClass}>Public key</th>
<th className={thRightClass}>Created</th>
<th className={thRightClass}>Actions</th>
@@ -67,7 +96,15 @@ export default function ClientsList({ host_id }: Props) {
</thead>
<tbody>
{clients.map((client) => (
<ClientRow key={client.id} client={client} />
<ClientRow
key={client.id}
client={client}
rules={
client.id
? rules_by_client_id.get(Number(client.id))
: undefined
}
/>
))}
</tbody>
</table>
@@ -0,0 +1,73 @@
import { useState } from "react";
import { Plus } from "lucide-react";
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
import Button from "@/src/components/twui/layout/Button";
import Stack from "@/src/components/twui/layout/Stack";
import Tag from "@/src/components/twui/elements/Tag";
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import ClientRuleFields from "../../(partials)/client-rules/client-rule-fields";
import {
clientRuleDraftToRecord,
createClientRuleDraft,
type ClientRuleDraft,
} from "../../(partials)/client-rules/client-rule-draft";
type Props = {
client: BUN_SQLITE_WGUI_CLIENTS;
};
export default function AddClientRuleForm({ client }: Props) {
const [draft, setDraft] = useState<ClientRuleDraft>(
createClientRuleDraft(),
);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>();
async function handleAdd() {
if (!client.id) {
setError("Client is missing an id");
return;
}
setBusy(true);
setError(undefined);
const res = await adminCrudHandler({
action: "insert",
table: "client_rules",
insert_data: [
{
...clientRuleDraftToRecord({ draft }),
client_id: client.id,
user_id: client.user_id,
},
],
});
if (!res.success) {
setBusy(false);
setError(res.msg || "Could not add rule");
return;
}
window.location.reload();
}
return (
<Stack className="w-full items-stretch gap-4">
{error ? <Tag color="error">{error}</Tag> : null}
<ClientRuleFields draft={draft} setDraft={setDraft} />
<Button
title="Add access rule"
type="button"
variant="outlined"
size="small"
beforeIcon={<Plus size={14} />}
loading={busy}
onClick={handleAdd}
>
Add rule
</Button>
</Stack>
);
}
@@ -0,0 +1,57 @@
import { useState } from "react";
import { Trash2 } from "lucide-react";
import type { BUN_SQLITE_WGUI_CLIENT_RULES } from "@/db/types/db";
import Button from "@/src/components/twui/layout/Button";
import Row from "@/src/components/twui/layout/Row";
import Span from "@/src/components/twui/layout/Span";
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import formatClientRuleLabel from "@/src/functions/frontend/format-client-rule-label";
type Props = {
rule: BUN_SQLITE_WGUI_CLIENT_RULES;
};
export default function ClientRuleRow({ rule }: Props) {
const [busy, setBusy] = useState(false);
async function handleDelete() {
if (!window.confirm(`Delete this access rule?`)) {
return;
}
setBusy(true);
const res = await adminCrudHandler({
action: "delete",
table: "client_rules",
id: rule.id,
});
if (!res.success) {
setBusy(false);
window.alert(res.msg || "Could not delete rule");
return;
}
window.location.reload();
}
return (
<Row className="justify-between gap-3 px-5 py-2.5 border-t border-slate-200/60 dark:border-white/5">
<Span className="font-mono text-[12.5px] text-foreground-light/70 dark:text-foreground-dark/70 break-all">
{formatClientRuleLabel({ rule })}
</Span>
<Button
title="Delete rule"
variant="ghost"
color="error"
size="smaller"
beforeIcon={<Trash2 size={14} />}
disabled={busy}
onClick={handleDelete}
>
{busy ? "Deleting…" : "Delete"}
</Button>
</Row>
);
}
@@ -4,6 +4,7 @@ import H3 from "@/src/components/twui/layout/H3";
import Row from "@/src/components/twui/layout/Row";
import Stack from "@/src/components/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import deriveClientAccessLabel from "@/src/functions/frontend/derive-client-access-label";
import ClientInfoRow from "../(partials)/client-info-row";
export default function ClientInfoSection() {
@@ -11,6 +12,9 @@ export default function ClientInfoSection() {
const client = pageProps?.client;
const host_id = Number(client?.host_id || 0);
const access = deriveClientAccessLabel({
rules: pageProps?.client_rules,
});
if (!client) {
return null;
@@ -31,6 +35,10 @@ export default function ClientInfoSection() {
value: client.allowed_ips || "—",
mono: true,
},
{
label: "Access",
value: access.text,
},
{
label: "Public key",
value: client.public_key || "—",
@@ -0,0 +1,87 @@
import { ShieldOff } from "lucide-react";
import { useContext } from "react";
import AdminCard from "@/src/components/general/admin-card";
import EmptyContent from "@/src/components/twui/elements/EmptyContent";
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 Stack from "@/src/components/twui/layout/Stack";
import { AppContext } from "@/src/pages/__root";
import AddClientRuleForm from "../(partials)/add-client-rule-form";
import ClientRuleRow from "../(partials)/client-rule-row";
import Modal from "@/src/components/twui/elements/Modal";
import useStatus from "@/src/components/twui/hooks/useStatus";
import Button from "@/src/components/twui/layout/Button";
import H4 from "@/src/components/twui/layout/H4";
import Span from "@/src/components/twui/layout/Span";
export default function ClientRulesSection() {
const { pageProps } = useContext(AppContext);
const client = pageProps?.client;
const rules = pageProps?.client_rules || [];
const { open, setOpen } = useStatus();
if (!client) {
return null;
}
return (
<AdminCard className="w-full overflow-hidden">
<Stack className="w-full items-stretch py-4 gap-4">
<Row className="w-full justify-between px-5">
<Stack className="gap-0.5">
<Row className="justify-between gap-3 flex-wrap">
<H3 className="mb-0!">Access rules</H3>
</Row>
<P
noMargin
className="text-[12.5px] text-foreground-light/45 dark:text-foreground-dark/45"
>
Zero trust: this client can only reach what you list
here.
</P>
</Stack>
<Modal
open={open}
setOpen={setOpen}
target={
<Button
title="Add Rule"
size="smaller"
variant="outlined"
>
Add Rule
</Button>
}
>
<Stack className="gap-10 w-full items-stretch">
<Stack className="gap-0.5">
<H4>Add Rule</H4>
<Span>
Add a new access rule to this client
</Span>
</Stack>
<AddClientRuleForm client={client} />
</Stack>
</Modal>
</Row>
{rules[0] ? (
<Stack className="w-full items-stretch">
{rules.map((rule) => (
<ClientRuleRow key={rule.id} rule={rule} />
))}
</Stack>
) : (
<EmptyContent
title="This client cannot reach anything until you add a rule"
icon={<ShieldOff size={16} />}
/>
)}
</Stack>
</AdminCard>
);
}
@@ -13,6 +13,7 @@ export default function EditClientFormSection() {
<AddClientForm
existing_client={pageProps?.client || undefined}
existing_client_full={pageProps?.client || undefined}
existing_client_rules={pageProps?.client_rules || undefined}
host_id={host_id}
/>
</Section>
@@ -1,4 +1,5 @@
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
import grabClientRules from "@/src/functions/backend/db/client-rules/grab-client-rules";
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
@@ -18,9 +19,16 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
targetId: page_query.client_id,
});
const client = client_res.singleRes || null;
const client_rules = await grabClientRules({
client_id: client?.id,
});
return {
props: {
client: client_res.singleRes || null,
client,
client_rules,
},
};
};
@@ -3,6 +3,7 @@ import type {
BUN_SQLITE_WGUI_HOSTS,
} from "@/db/types/db";
import { AppData } from "@/src/data/app-data";
import grabClientRules from "@/src/functions/backend/db/client-rules/grab-client-rules";
import generateWireguardClientQR from "@/src/functions/backend/setup/generate-wireguard-client-qr";
import readWireguardClientConfig from "@/src/functions/backend/setup/read-wireguard-client-config";
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
@@ -55,6 +56,10 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
config: config_res.config,
});
const client_rules = await grabClientRules({
client_id: client?.id,
});
return {
props: {
client,
@@ -62,6 +67,7 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
client_config: config_res.config,
client_config_path: config_res.config_path,
client_qr_data_uri: qr_res.qr_data_uri,
client_rules,
},
};
};
@@ -11,6 +11,7 @@ import EmptyContent from "@/src/components/twui/elements/EmptyContent";
import ClientInfoSection from "./(sections)/client-info-section";
import ClientConfigSection from "./(sections)/client-config-section";
import ClientQRSection from "./(sections)/client-qr-section";
import ClientRulesSection from "./(sections)/client-rules-section";
import ClientDetailActions from "./(partials)/client-detail-actions";
export default function AdminClientDetailPage() {
@@ -52,6 +53,7 @@ export default function AdminClientDetailPage() {
{pageProps?.client ? (
<>
<ClientInfoSection />
<ClientRulesSection />
<ClientQRSection />
<ClientConfigSection />
</>
@@ -1,10 +1,20 @@
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
import type {
BUN_SQLITE_WGUI_CLIENT_RULES,
BUN_SQLITE_WGUI_CLIENTS,
} from "@/db/types/db";
import adminCrudHandler from "@/src/functions/frontend/admin-crud-handler";
import type useFormInit from "@/src/hooks/use-form-init";
import _ from "lodash";
import {
clientRuleDraftToRecord,
type ClientRuleDraft,
} from "../../(partials)/client-rules/client-rule-draft";
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
import type { ApiReqParams } from "@/src/types";
type Params = {
host_id?: string | number;
rules?: ClientRuleDraft[];
};
export default async function submitAddClientForm(
@@ -15,7 +25,7 @@ export default async function submitAddClientForm(
existing_full,
app_context,
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_CLIENTS>>,
{ host_id }: Params,
{ host_id, rules }: Params,
) {
try {
const confirm_msg = existing_full?.id
@@ -44,6 +54,10 @@ export default async function submitAddClientForm(
setLoading(true);
const client_rules = (rules || []).map((draft) =>
clientRuleDraftToRecord({ draft }),
);
const res = existing_full?.id
? await adminCrudHandler({
action: "update",
@@ -57,6 +71,47 @@ export default async function submitAddClientForm(
insert_data: [new_client_data],
});
const client_id = existing_full?.id || res.postInsertReturn?.insertId;
const delete_client_rules =
await adminCrudHandler<BUN_SQLITE_WGUI_CLIENT_RULES>({
action: "delete",
table: "client_rules",
sql_query: {
query: {
client_id: {
value: String(client_id),
},
host_id: {
value: String(host_id),
},
},
},
});
if (client_rules?.[0] && client_id) {
const insert_client_rules =
await adminCrudHandler<BUN_SQLITE_WGUI_CLIENT_RULES>({
action: "insert",
table: "client_rules",
insert_data: client_rules.map((cr) => ({
...cr,
host_id: Number(host_id),
client_id: Number(client_id),
user_id: app_context.user?.id,
})),
});
}
if (client_id) {
await fetchApi<ApiReqParams>(`/api/admin/run-client-setup`, {
method: "POST",
body: {
client_id,
},
});
}
if (res.success) {
if (existing_full?.id) {
window.location.pathname = `/admin/hosts/${final_host_id}/clients/${existing_full.id}`;
@@ -10,7 +10,7 @@ export default function AddClientFormAllowedIps({
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_CLIENTS>>) {
return (
<Stack className="gap-2 my-4 w-full">
{form.allow_all_ips ? null : (
{form.allow_all_ips == 1 ? null : (
<Input
title="Allowed IPs"
placeholder="Eg. 10.0.0.2/24"
@@ -24,8 +24,9 @@ export default function AddClientFormAllowedIps({
showLabel
/>
)}
<Checkbox
label={`Allow all IPs? That is 0.0.0.0/0, ::/0`}
label={`Allow all IPs?`}
defaultChecked={form.allow_all_ips == 1}
changeHandler={(value) => {
setForm((prev) => ({
@@ -33,6 +34,12 @@ export default function AddClientFormAllowedIps({
allow_all_ips: value ? 1 : 0,
}));
}}
info={
<span>
Sets the wireguard <code>AllowedIPs</code> parameter to{" "}
<code>0.0.0.0/0, ::/0</code>
</span>
}
/>
</Stack>
);
@@ -1,25 +1,37 @@
import type { BUN_SQLITE_WGUI_CLIENTS } from "@/db/types/db";
import { useState } from "react";
import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_CLIENT_RULES,
} from "@/db/types/db";
import Form from "@/src/components/twui/form/Form";
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
import Stack from "@/src/components/twui/layout/Stack";
import Tag from "@/src/components/twui/elements/Tag";
import useFormInit from "@/src/hooks/use-form-init";
import submitAddClientForm from "../../(functions)/submit-add-client-form";
import ClientRulesEditor from "../../../(partials)/client-rules/client-rules-editor";
import {
clientRuleToDraft,
type ClientRuleDraft,
} from "../../../(partials)/client-rules/client-rule-draft";
import AddClientFormAction from "./add-client-form-action";
import AddClientFormAllowedIps from "./add-client-form-allowed-ips";
import AddClientFormNotes from "./add-client-form-notes";
import AddClientFormTitle from "./add-client-form-title";
import AddClientFormWgIpAddress from "./add-client-form-wg-ip-address";
import LoadingRectangleBlock from "@/src/components/twui/layout/LoadingRectangleBlock";
type Props = {
existing_client?: BUN_SQLITE_WGUI_CLIENTS;
existing_client_full?: BUN_SQLITE_WGUI_CLIENTS;
existing_client_rules?: BUN_SQLITE_WGUI_CLIENT_RULES[];
host_id?: string | number;
};
export default function AddClientForm({
existing_client,
existing_client_full,
existing_client_rules,
host_id,
}: Props) {
const init = useFormInit<BUN_SQLITE_WGUI_CLIENTS>({
@@ -28,25 +40,35 @@ export default function AddClientForm({
title: "Add Client",
});
const [rules, setRules] = useState<ClientRuleDraft[]>(
(existing_client_rules || []).map((rule) =>
clientRuleToDraft({ rule }),
),
);
const { loading, status } = init;
return (
<Form
className="w-full gap-6"
onSubmit={() => {
submitAddClientForm(init, { host_id });
submitAddClientForm(init, { host_id, rules });
}}
>
{status?.error && <Tag color="error">{status.msg}</Tag>}
{loading && <LoadingOverlay />}
<Stack>
<AddClientFormTitle {...init} />
<AddClientFormWgIpAddress {...init} host_id={host_id} />
<AddClientFormAllowedIps {...init} />
<AddClientFormNotes {...init} />
<AddClientFormAction {...init} />
</Stack>
{init.ready ? (
<Stack>
<AddClientFormTitle {...init} />
<AddClientFormWgIpAddress {...init} host_id={host_id} />
<AddClientFormAllowedIps {...init} />
<AddClientFormNotes {...init} />
<ClientRulesEditor rules={rules} setRules={setRules} />
<AddClientFormAction {...init} />
</Stack>
) : (
<LoadingRectangleBlock className="h-[400px]" />
)}
</Form>
);
}
}
@@ -2,6 +2,7 @@ import type {
BUN_SQLITE_WGUI_CLIENTS,
BUN_SQLITE_WGUI_HOSTS,
} from "@/db/types/db";
import grabClientRules from "@/src/functions/backend/db/client-rules/grab-client-rules";
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
import BunSQLite from "@moduletrace/bun-sqlite";
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
@@ -32,10 +33,19 @@ const server: BunextPageServerFn<PagePropsType> = async ({
},
});
const clients = host_clients.payload || null;
const client_rules = await grabClientRules({
client_ids: (clients || [])
.map((client) => client.id)
.filter((id): id is number => Boolean(id)),
});
return {
props: {
host: host_record_res.singleRes || null,
clients: host_clients.payload || null,
clients,
client_rules,
},
};
};