This commit is contained in:
Milo Schwartz
2024-12-24 15:37:04 -05:00
6 changed files with 141 additions and 17 deletions

View File

@@ -30,6 +30,9 @@ import {
CardHeader,
CardTitle
} from "@/components/ui/card";
import { AxiosResponse } from "axios";
import { DeleteOrgResponse, ListOrgsResponse } from "@server/routers/org";
import { redirect, useRouter } from "next/navigation";
const GeneralFormSchema = z.object({
name: z.string()
@@ -41,6 +44,7 @@ export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { orgUser } = userOrgUserContext();
const router = useRouter();
const { org } = useOrgContext();
const { toast } = useToast();
const api = createApiClient(useEnvContext());
@@ -54,16 +58,54 @@ export default function GeneralPage() {
});
async function deleteOrg() {
await api.delete(`/org/${org?.org.orgId}`).catch((e) => {
try {
const res = await api.delete<AxiosResponse<DeleteOrgResponse>>(
`/org/${org?.org.orgId}`
);
if (res.status === 200) {
pickNewOrgAndNavigate();
}
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: "Failed to delete org",
description: formatAxiosError(
e,
err,
"An error occurred while deleting the org."
)
});
});
}
}
async function pickNewOrgAndNavigate() {
try {
const res = await api.get<AxiosResponse<ListOrgsResponse>>(
`/orgs`
);
if (res.status === 200) {
if (res.data.data.orgs.length > 0) {
const orgId = res.data.data.orgs[0].orgId;
// go to `/${orgId}/settings`);
router.push(`/${orgId}/settings`);
} else {
// go to `/setup`
router.push("/setup");
}
}
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: "Failed to fetch orgs",
description: formatAxiosError(
err,
"An error occurred while listing your orgs"
)
});
}
}
async function onSubmit(data: GeneralFormValues) {

View File

@@ -51,6 +51,7 @@ import { ArrayElement } from "@server/types/ArrayElement";
import { formatAxiosError } from "@app/lib/utils";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { createApiClient } from "@app/api";
import { GetSiteResponse } from "@server/routers/site";
const addTargetSchema = z.object({
ip: z.string().ip(),
@@ -85,6 +86,7 @@ export default function ReverseProxyTargets(props: {
const api = createApiClient(useEnvContext());
const [targets, setTargets] = useState<LocalTarget[]>([]);
const [site, setSite] = useState<GetSiteResponse>();
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
const [sslEnabled, setSslEnabled] = useState(resource.ssl);
@@ -103,7 +105,7 @@ export default function ReverseProxyTargets(props: {
});
useEffect(() => {
const fetchSites = async () => {
const fetchTargets = async () => {
try {
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
`/resource/${params.resourceId}/targets`,
@@ -126,7 +128,30 @@ export default function ReverseProxyTargets(props: {
setPageLoading(false);
}
};
fetchSites();
fetchTargets();
const fetchSite = async () => {
try {
const res = await api.get<AxiosResponse<GetSiteResponse>>(
`/site/${resource.siteId}`,
);
if (res.status === 200) {
setSite(res.data.data);
}
} catch (err) {
console.error(err);
toast({
variant: "destructive",
title: "Failed to fetch resource",
description: formatAxiosError(
err,
"An error occurred while fetching resource",
),
});
}
}
fetchSite();
}, []);
async function addTarget(data: AddTargetFormValues) {
@@ -146,6 +171,20 @@ export default function ReverseProxyTargets(props: {
return;
}
if (site && site.type == "wireguard" && site.subnet) {
// make sure that the target IP is within the site subnet
const targetIp = data.ip;
const subnet = site.subnet;
if (!isIPInSubnet(targetIp, subnet)) {
toast({
variant: "destructive",
title: "Invalid target IP",
description: "Target IP must be within the site subnet",
});
return;
}
}
const newTarget: LocalTarget = {
...data,
enabled: true,
@@ -602,3 +641,40 @@ export default function ReverseProxyTargets(props: {
</>
);
}
function isIPInSubnet(subnet: string, ip: string): boolean {
// Split subnet into IP and mask parts
const [subnetIP, maskBits] = subnet.split('/');
const mask = parseInt(maskBits);
if (mask < 0 || mask > 32) {
throw new Error('Invalid subnet mask. Must be between 0 and 32.');
}
// Convert IP addresses to binary numbers
const subnetNum = ipToNumber(subnetIP);
const ipNum = ipToNumber(ip);
// Calculate subnet mask
const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1);
// Check if the IP is in the subnet
return (subnetNum & maskNum) === (ipNum & maskNum);
}
function ipToNumber(ip: string): number {
// Validate IP address format
const parts = ip.split('.');
if (parts.length !== 4) {
throw new Error('Invalid IP address format');
}
// Convert IP octets to 32-bit number
return parts.reduce((num, octet) => {
const oct = parseInt(octet);
if (isNaN(oct) || oct < 0 || oct > 255) {
throw new Error('Invalid IP address octet');
}
return (num << 8) + oct;
}, 0);
}