Updates
This commit is contained in:
@@ -76,7 +76,9 @@ export default function ClientRow({ client, rules }: Props) {
|
||||
{client.wg_ip_address || "—"}
|
||||
</td>
|
||||
<td className="px-4 py-[9px] tabular text-[13px] text-foreground-light/45 dark:text-foreground-dark/45 whitespace-nowrap">
|
||||
{client.allowed_ips || "—"}
|
||||
{client.allow_all_ips
|
||||
? "0.0.0.0/0, ::/0"
|
||||
: client.allowed_ips || "—"}
|
||||
</td>
|
||||
<td className="px-4 py-[9px] whitespace-nowrap">
|
||||
<Span
|
||||
|
||||
@@ -104,9 +104,11 @@ export default async function setupWireguardHost({
|
||||
|
||||
const is_ip_available = is_update_after_client_setup
|
||||
? { success: true }
|
||||
: await checkPrivateIPAvailability({
|
||||
ip_address: HOST_WG_IP,
|
||||
});
|
||||
: host?.wg_ip_address
|
||||
? { success: true }
|
||||
: await checkPrivateIPAvailability({
|
||||
ip_address: HOST_WG_IP,
|
||||
});
|
||||
|
||||
if (!is_ip_available.success && !is_update_after_client_setup) {
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,12 @@ import Stack from "@/src/components/twui/layout/Stack";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import AdminCard from "@/src/components/general/admin-card";
|
||||
import ClientRow from "@/src/components/general/client-row";
|
||||
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 { useAdminCrudGet } from "@/src/hooks/use-admin-crud-get";
|
||||
import { useMemo } from "react";
|
||||
|
||||
type Props = {
|
||||
clients?: BUN_SQLITE_WGUI_CLIENTS[];
|
||||
@@ -23,6 +28,34 @@ const RECENT_LIMIT = 8;
|
||||
export default function PeersTableSection({ clients }: Props) {
|
||||
const recent = (clients || []).slice(0, RECENT_LIMIT);
|
||||
|
||||
const { res: client_rules } = useAdminCrudGet<BUN_SQLITE_WGUI_CLIENT_RULES>(
|
||||
{
|
||||
table: "client_rules",
|
||||
sql_query: {
|
||||
limit: 500,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
return (
|
||||
<AdminCard className="w-full overflow-hidden">
|
||||
<Row className="justify-between gap-3 px-5 h-12 flex-wrap">
|
||||
@@ -40,24 +73,40 @@ export default function PeersTableSection({ clients }: Props) {
|
||||
{recent.length ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full min-w-[860px]">
|
||||
<thead>
|
||||
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03]">
|
||||
<thead className="w-full">
|
||||
<tr className="border-y border-slate-200 dark:border-white/10 bg-foreground-light/[0.02] dark:bg-foreground-dark/[0.03] w-full">
|
||||
<th className={thClass}>Client</th>
|
||||
<th className={thClass}>Host</th>
|
||||
<th className={thClass}>Tunnel IP</th>
|
||||
<th className={thClass}>Allowed IPs</th>
|
||||
<th className={thClass}>Access List</th>
|
||||
<th className={thClass}>Public key</th>
|
||||
<th className={thRightClass}>Created</th>
|
||||
<th className={thRightClass}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recent.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>
|
||||
</div>
|
||||
) : (
|
||||
<Stack center className="w-full justify-center py-14 px-6 text-center gap-1">
|
||||
<Stack
|
||||
center
|
||||
className="w-full justify-center py-14 px-6 text-center gap-1"
|
||||
>
|
||||
<Span className="text-[13.5px] font-medium text-foreground-light/50 dark:text-foreground-dark/50">
|
||||
No clients yet
|
||||
</Span>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||
import getInterfaceLocalIP from "@/src/functions/backend/setup/get-interface-local-ip";
|
||||
import grabHostNetworkInterfaces from "@/src/functions/backend/setup/grab-host-network-interfaces";
|
||||
import grabHostPublicIPAddress from "@/src/functions/backend/setup/grab-host-public-ip-address";
|
||||
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
@@ -8,19 +11,23 @@ const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
|
||||
|
||||
const host_id = Number(page_query.host_id || 0);
|
||||
|
||||
const host_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
TableType
|
||||
>({
|
||||
const host_res = await BunSQLite.select<BUN_SQLITE_WGUI_HOSTS, TableType>({
|
||||
table: "hosts",
|
||||
targetId: host_id,
|
||||
});
|
||||
|
||||
const host = host_res.singleRes || null;
|
||||
|
||||
const network_interfaces = await grabHostNetworkInterfaces();
|
||||
const local_ip = host?.interface
|
||||
? await getInterfaceLocalIP(host?.interface)
|
||||
: null;
|
||||
|
||||
return {
|
||||
props: {
|
||||
host,
|
||||
network_interfaces,
|
||||
is_local_ip: local_ip == host?.public_ip_address,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,6 +9,9 @@ import { ArrowLeft } from "lucide-react";
|
||||
import { useContext } from "react";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import EditHostFormSection from "./(sections)/edit-host-form-section";
|
||||
import AddHostForm from "../../add/(partials)/add-host-form/add-host-form";
|
||||
import _ from "lodash";
|
||||
import type { BUN_SQLITE_WGUI_HOSTS_JOIN } from "@/src/types/sql-joins";
|
||||
|
||||
export default function AdminEditHostPage() {
|
||||
const { pageProps, query } = useContext(AppContext);
|
||||
@@ -40,7 +43,13 @@ export default function AdminEditHostPage() {
|
||||
|
||||
<Stack className="w-full px-6 pb-8 gap-5 items-stretch">
|
||||
{pageProps?.host ? (
|
||||
<EditHostFormSection />
|
||||
<AddHostForm
|
||||
existing_host={_.pick<
|
||||
BUN_SQLITE_WGUI_HOSTS_JOIN,
|
||||
keyof BUN_SQLITE_WGUI_HOSTS_JOIN
|
||||
>(pageProps.host, ["name", "short_description"])}
|
||||
existing_host_full={pageProps.host}
|
||||
/>
|
||||
) : (
|
||||
<EmptyContent title="Host not found" />
|
||||
)}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { BUN_SQLITE_WGUI_CLIENTS, BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_CLIENTS,
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
} from "@/db/types/db";
|
||||
import grabHostDirnames from "@/src/functions/backend/setup/grab-host-dir-names";
|
||||
import type { PagePropsType, PageQueryObject, TableType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
const server: BunextPageServerFn<PagePropsType> = async ({
|
||||
req,
|
||||
url,
|
||||
query,
|
||||
}) => {
|
||||
const server: BunextPageServerFn<PagePropsType> = async ({ query }) => {
|
||||
const page_query = query as PageQueryObject;
|
||||
|
||||
const host_record_res = await BunSQLite.select<
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function AdminSingleHostPage() {
|
||||
color="primary"
|
||||
href={`/admin/hosts/${host_id}/clients`}
|
||||
>
|
||||
View Clients
|
||||
Clients
|
||||
</Button>
|
||||
<Button
|
||||
title="Add Client"
|
||||
|
||||
@@ -10,36 +10,53 @@ export default async function submitAddHostForm({
|
||||
form,
|
||||
app_context,
|
||||
clearLocalForm,
|
||||
existing_full,
|
||||
}: ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>) {
|
||||
try {
|
||||
if (!window.confirm("Create this new host?")) {
|
||||
const confirm_msg = existing_full?.id
|
||||
? `Update this host?`
|
||||
: `Create this new host?`;
|
||||
|
||||
if (!window.confirm(confirm_msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/admin/add-host`,
|
||||
existing_full?.id
|
||||
? `/api/admin/update-host`
|
||||
: `/api/admin/add-host`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
wg_ip_address: form.wg_ip_address || "",
|
||||
public_ip_address: form.public_ip_address || "",
|
||||
interface: form.interface || "",
|
||||
name: form.name || "",
|
||||
short_description: form.short_description || "",
|
||||
listen_port:
|
||||
form.listen_port != null
|
||||
? Number(form.listen_port)
|
||||
: null,
|
||||
is_main_host: app_context.pageProps?.is_main_host,
|
||||
},
|
||||
body: existing_full?.id
|
||||
? {
|
||||
host_id: existing_full.id,
|
||||
update_data: form,
|
||||
}
|
||||
: {
|
||||
wg_ip_address: form.wg_ip_address || "",
|
||||
public_ip_address: form.public_ip_address || "",
|
||||
interface: form.interface || "",
|
||||
name: form.name || "",
|
||||
short_description: form.short_description || "",
|
||||
listen_port:
|
||||
form.listen_port != null
|
||||
? Number(form.listen_port)
|
||||
: null,
|
||||
is_main_host: app_context.pageProps?.is_main_host,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const host_id =
|
||||
typeof existing_full?.id == "number"
|
||||
? existing_full.id
|
||||
: res.numberRes;
|
||||
|
||||
if (res.success) {
|
||||
clearLocalForm();
|
||||
window.location.pathname = `/admin/hosts/${res.numberRes}`;
|
||||
window.location.pathname = `/admin/hosts/${host_id}`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>> & {
|
||||
export default function AddHostFormAction({
|
||||
status,
|
||||
ipStatus,
|
||||
existing_full,
|
||||
}: Props) {
|
||||
if (status?.error) {
|
||||
return null;
|
||||
@@ -19,9 +20,9 @@ export default function AddHostFormAction({
|
||||
<Button
|
||||
title="Create Host"
|
||||
type="submit"
|
||||
disabled={ipStatus !== "available"}
|
||||
disabled={existing_full?.id ? false : ipStatus !== "available"}
|
||||
>
|
||||
Create Host
|
||||
{existing_full?.id ? "Update Host" : "Create Host"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import Select from "@/src/components/twui/form/Select";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
@@ -9,9 +10,21 @@ export default function AddHostFormInterface({
|
||||
form,
|
||||
setForm,
|
||||
app_context,
|
||||
existing_full,
|
||||
}: Props) {
|
||||
const interfaces = app_context.pageProps?.network_interfaces || [];
|
||||
|
||||
if (existing_full?.id) {
|
||||
return (
|
||||
<Input
|
||||
value={existing_full.interface || "N/A"}
|
||||
title="Network Interface"
|
||||
readOnly
|
||||
showLabel
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-2 items-stretch">
|
||||
<Select
|
||||
|
||||
@@ -12,9 +12,24 @@ import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||
|
||||
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
|
||||
|
||||
export default function AddHostFormListenPort({ form, setForm }: Props) {
|
||||
export default function AddHostFormListenPort({
|
||||
form,
|
||||
setForm,
|
||||
existing_full,
|
||||
}: Props) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
if (existing_full?.id) {
|
||||
return (
|
||||
<Input
|
||||
value={existing_full.listen_port || "N/A"}
|
||||
title="Listen Port"
|
||||
readOnly
|
||||
showLabel
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleGrabNext() {
|
||||
setBusy(true);
|
||||
try {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import Toggle from "@/src/components/twui/elements/Toggle";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { useEffect, useState } from "react";
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
@@ -16,10 +13,17 @@ import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
|
||||
|
||||
type Props = ReturnType<typeof useFormInit<BUN_SQLITE_WGUI_HOSTS>>;
|
||||
|
||||
export default function AddHostFormPublicIP({ form, setForm }: Props) {
|
||||
export default function AddHostFormPublicIP({
|
||||
form,
|
||||
setForm,
|
||||
app_context,
|
||||
existing_full,
|
||||
}: Props) {
|
||||
const [useLocalIP, setUseLocalIP] = useState(false);
|
||||
|
||||
const { loading, setLoading } = useStatus({ initialLoading: true });
|
||||
const { loading, setLoading } = useStatus({
|
||||
initialLoading: existing_full?.id ? false : true,
|
||||
});
|
||||
|
||||
function fetchIP(iface: string, local: boolean) {
|
||||
if (!iface) return;
|
||||
@@ -53,6 +57,17 @@ export default function AddHostFormPublicIP({ form, setForm }: Props) {
|
||||
fetchIP(form.interface, useLocalIP);
|
||||
}, [form.interface, useLocalIP]);
|
||||
|
||||
if (existing_full?.id) {
|
||||
return (
|
||||
<Input
|
||||
value={existing_full.public_ip_address || "N/A"}
|
||||
title="IP Address"
|
||||
readOnly
|
||||
showLabel
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack className={twMerge("w-full gap-2 relative")}>
|
||||
{loading ? <LoadingOverlay /> : null}
|
||||
@@ -76,7 +91,9 @@ export default function AddHostFormPublicIP({ form, setForm }: Props) {
|
||||
label={
|
||||
<span className="text-base opacity-80">Use Local IP?</span>
|
||||
}
|
||||
defaultChecked={useLocalIP}
|
||||
defaultChecked={
|
||||
useLocalIP || Boolean(app_context.pageProps?.is_local_ip)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import GrabNextSubnetButton from "@/src/components/general/grab-next-subnet-butt
|
||||
import HostWgIpField, {
|
||||
type HostWgIpFieldStatus,
|
||||
} from "@/src/components/general/host-wg-ip-field";
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
|
||||
@@ -14,6 +15,17 @@ export default function AddHostFormWgIpAddress({
|
||||
setIpStatus,
|
||||
...props
|
||||
}: Props) {
|
||||
if (props.existing_full?.wg_ip_address) {
|
||||
return (
|
||||
<Input
|
||||
placeholder="WG IP address"
|
||||
value={props.existing_full.wg_ip_address}
|
||||
readOnly
|
||||
showLabel
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-2">
|
||||
<HostWgIpField
|
||||
|
||||
@@ -8,7 +8,6 @@ import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import submitAddHostForm from "../../(functions)/submit-add-host-form";
|
||||
import AddHostFormAction from "./add-host-form-action";
|
||||
import AddHostFormSubnet from "./add-host-form-subnet";
|
||||
import AddHostFormWgIpAddress from "./add-host-form-wg-ip-address";
|
||||
import AddHostFormPublicIP from "./add-host-form-public-ip";
|
||||
import AddHostFormInterface from "./add-host-form-interface";
|
||||
@@ -18,7 +17,15 @@ import AddHostFormListenPort from "./add-host-form-listen-port";
|
||||
import LoadingRectangleBlock from "@/src/components/twui/layout/LoadingRectangleBlock";
|
||||
import type { HostWgIpFieldStatus } from "@/src/components/general/host-wg-ip-field";
|
||||
|
||||
export default function AddHostForm() {
|
||||
type Props = {
|
||||
existing_host?: BUN_SQLITE_WGUI_HOSTS;
|
||||
existing_host_full?: BUN_SQLITE_WGUI_HOSTS;
|
||||
};
|
||||
|
||||
export default function AddHostForm({
|
||||
existing_host,
|
||||
existing_host_full,
|
||||
}: Props) {
|
||||
const init = useFormInit<BUN_SQLITE_WGUI_HOSTS>({
|
||||
default: {
|
||||
wg_ip_address: AppData["DefaultPrivateIP"],
|
||||
@@ -26,6 +33,8 @@ export default function AddHostForm() {
|
||||
},
|
||||
title: "new_host",
|
||||
async before_ready_function(params) {
|
||||
if (existing_host_full?.id) return;
|
||||
|
||||
params.setForm((prev) => ({
|
||||
...prev,
|
||||
interface:
|
||||
@@ -38,6 +47,8 @@ export default function AddHostForm() {
|
||||
undefined,
|
||||
}));
|
||||
},
|
||||
existing: existing_host,
|
||||
existing_full: existing_host_full,
|
||||
});
|
||||
|
||||
const [ipStatus, setIpStatus] = useState<HostWgIpFieldStatus>("checking");
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { BUN_SQLITE_WGUI_HOSTS } from "@/db/types/db";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import createWireguardHost from "@/src/functions/backend/setup/create-wireguard-host";
|
||||
import setupWireguardHost from "@/src/functions/backend/setup/setup-wireguard-host";
|
||||
import type { ApiReqParams, TableType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
const req = params.req;
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const is_super_admin = checkUserAccess({ user_types });
|
||||
|
||||
if (!is_super_admin.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof body.host_id !== "number") {
|
||||
throw new Error(`Invalid host id`);
|
||||
}
|
||||
|
||||
const host_record_res = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
TableType
|
||||
>({
|
||||
table: "hosts",
|
||||
targetId: body.host_id,
|
||||
});
|
||||
|
||||
const host = host_record_res.singleRes;
|
||||
|
||||
if (!host?.created_at) {
|
||||
throw new Error(`Couldn't find host record`);
|
||||
}
|
||||
|
||||
if (!body.update_data) {
|
||||
throw new Error(`No data to update`);
|
||||
}
|
||||
|
||||
const update_host_record = await BunSQLite.update<
|
||||
BUN_SQLITE_WGUI_HOSTS,
|
||||
TableType
|
||||
>({
|
||||
table: "hosts",
|
||||
data: body.update_data,
|
||||
targetId: String(host.id),
|
||||
});
|
||||
|
||||
const update = await setupWireguardHost({
|
||||
host,
|
||||
user,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -70,6 +70,7 @@ export type PagePropsType = {
|
||||
is_main_host?: boolean | null;
|
||||
main_host?: BUN_SQLITE_WGUI_HOSTS | null;
|
||||
next_available_client_ip?: string | null;
|
||||
is_local_ip?: boolean | null;
|
||||
};
|
||||
|
||||
export type AppContextObject = {
|
||||
|
||||
Reference in New Issue
Block a user