Преглед изворни кода

Merge branch 'main' of https://github.com/fosrl/pangolin

Milo Schwartz пре 6 месеци
родитељ
комит
b3d371c01e

+ 4 - 2
server/db/schema.ts

@@ -130,8 +130,10 @@ export const userOrgs = sqliteTable("userOrgs", {
         .notNull()
         .notNull()
         .references(() => users.userId),
         .references(() => users.userId),
     orgId: text("orgId")
     orgId: text("orgId")
-        .notNull()
-        .references(() => orgs.orgId),
+        .references(() => orgs.orgId, {
+            onDelete: "cascade"
+        })
+        .notNull(),
     roleId: integer("roleId")
     roleId: integer("roleId")
         .notNull()
         .notNull()
         .references(() => roles.roleId),
         .references(() => roles.roleId),

+ 2 - 1
server/middlewares/verifyUserIsOrgOwner.ts

@@ -27,7 +27,6 @@ export async function verifyUserIsOrgOwner(
             )
             )
         );
         );
     }
     }
-
     try {
     try {
         if (!req.userOrg) {
         if (!req.userOrg) {
             const res = await db
             const res = await db
@@ -56,6 +55,8 @@ export async function verifyUserIsOrgOwner(
                 )
                 )
             );
             );
         }
         }
+        
+        return next();
     } catch (e) {
     } catch (e) {
         return next(
         return next(
             createHttpError(
             createHttpError(

+ 9 - 8
server/routers/org/deleteOrg.ts

@@ -24,6 +24,10 @@ const deleteOrgSchema = z
     })
     })
     .strict();
     .strict();
 
 
+export type DeleteOrgResponse = {
+
+}
+
 export async function deleteOrg(
 export async function deleteOrg(
     req: Request,
     req: Request,
     res: Response,
     res: Response,
@@ -41,7 +45,6 @@ export async function deleteOrg(
         }
         }
 
 
         const { orgId } = parsedParams.data;
         const { orgId } = parsedParams.data;
-
         // Check if the user has permission to list sites
         // Check if the user has permission to list sites
         const hasPermission = await checkUserActionPermission(
         const hasPermission = await checkUserActionPermission(
             ActionsEnum.deleteOrg,
             ActionsEnum.deleteOrg,
@@ -55,7 +58,6 @@ export async function deleteOrg(
                 )
                 )
             );
             );
         }
         }
-
         const [org] = await db
         const [org] = await db
             .select()
             .select()
             .from(orgs)
             .from(orgs)
@@ -70,7 +72,6 @@ export async function deleteOrg(
                 )
                 )
             );
             );
         }
         }
-
         // we need to handle deleting each site
         // we need to handle deleting each site
         const orgSites = await db
         const orgSites = await db
             .select()
             .select()
@@ -97,20 +98,20 @@ export async function deleteOrg(
                             sendToClient(deletedNewt.newtId, payload);
                             sendToClient(deletedNewt.newtId, payload);
 
 
                             // delete all of the sessions for the newt
                             // delete all of the sessions for the newt
-                            db.delete(newtSessions)
+                            await db.delete(newtSessions)
                                 .where(
                                 .where(
                                     eq(newtSessions.newtId, deletedNewt.newtId)
                                     eq(newtSessions.newtId, deletedNewt.newtId)
-                                )
-                                .run();
+                                );
                         }
                         }
                     }
                     }
                 }
                 }
 
 
-                db.delete(sites).where(eq(sites.siteId, site.siteId)).run();
+                logger.info(`Deleting site ${site.siteId}`);
+                await db.delete(sites).where(eq(sites.siteId, site.siteId))
             }
             }
         }
         }
 
 
-        await db.delete(orgs).where(eq(orgs.orgId, orgId)).returning();
+        await db.delete(orgs).where(eq(orgs.orgId, orgId));
 
 
         return response(res, {
         return response(res, {
             data: null,
             data: null,

+ 3 - 1
server/routers/site/getSite.ts

@@ -28,6 +28,7 @@ export type GetSiteResponse = {
     name: string;
     name: string;
     subdomain: string;
     subdomain: string;
     subnet: string;
     subnet: string;
+    type: string;
 };
 };
 
 
 export async function getSite(
 export async function getSite(
@@ -81,7 +82,8 @@ export async function getSite(
                 siteId: site[0].siteId,
                 siteId: site[0].siteId,
                 niceId: site[0].niceId,
                 niceId: site[0].niceId,
                 name: site[0].name,
                 name: site[0].name,
-                subnet: site[0].subnet
+                subnet: site[0].subnet,
+                type: site[0].type
             },
             },
             success: true,
             success: true,
             error: false,
             error: false,

+ 45 - 3
src/app/[orgId]/settings/general/page.tsx

@@ -30,6 +30,9 @@ import {
     CardHeader,
     CardHeader,
     CardTitle
     CardTitle
 } from "@/components/ui/card";
 } 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({
 const GeneralFormSchema = z.object({
     name: z.string()
     name: z.string()
@@ -41,6 +44,7 @@ export default function GeneralPage() {
     const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
     const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
 
 
     const { orgUser } = userOrgUserContext();
     const { orgUser } = userOrgUserContext();
+    const router = useRouter();
     const { org } = useOrgContext();
     const { org } = useOrgContext();
     const { toast } = useToast();
     const { toast } = useToast();
     const api = createApiClient(useEnvContext());
     const api = createApiClient(useEnvContext());
@@ -54,16 +58,54 @@ export default function GeneralPage() {
     });
     });
 
 
     async function deleteOrg() {
     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({
             toast({
                 variant: "destructive",
                 variant: "destructive",
                 title: "Failed to delete org",
                 title: "Failed to delete org",
                 description: formatAxiosError(
                 description: formatAxiosError(
-                    e,
+                    err,
                     "An error occurred while deleting the org."
                     "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) {
     async function onSubmit(data: GeneralFormValues) {

+ 78 - 2
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -51,6 +51,7 @@ import { ArrayElement } from "@server/types/ArrayElement";
 import { formatAxiosError } from "@app/lib/utils";
 import { formatAxiosError } from "@app/lib/utils";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import { createApiClient } from "@app/api";
 import { createApiClient } from "@app/api";
+import { GetSiteResponse } from "@server/routers/site";
 
 
 const addTargetSchema = z.object({
 const addTargetSchema = z.object({
     ip: z.string().ip(),
     ip: z.string().ip(),
@@ -85,6 +86,7 @@ export default function ReverseProxyTargets(props: {
     const api = createApiClient(useEnvContext());
     const api = createApiClient(useEnvContext());
 
 
     const [targets, setTargets] = useState<LocalTarget[]>([]);
     const [targets, setTargets] = useState<LocalTarget[]>([]);
+    const [site, setSite] = useState<GetSiteResponse>();
     const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
     const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
     const [sslEnabled, setSslEnabled] = useState(resource.ssl);
     const [sslEnabled, setSslEnabled] = useState(resource.ssl);
 
 
@@ -103,7 +105,7 @@ export default function ReverseProxyTargets(props: {
     });
     });
 
 
     useEffect(() => {
     useEffect(() => {
-        const fetchSites = async () => {
+        const fetchTargets = async () => {
             try {
             try {
                 const res = await api.get<AxiosResponse<ListTargetsResponse>>(
                 const res = await api.get<AxiosResponse<ListTargetsResponse>>(
                     `/resource/${params.resourceId}/targets`,
                     `/resource/${params.resourceId}/targets`,
@@ -126,7 +128,30 @@ export default function ReverseProxyTargets(props: {
                 setPageLoading(false);
                 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) {
     async function addTarget(data: AddTargetFormValues) {
@@ -146,6 +171,20 @@ export default function ReverseProxyTargets(props: {
             return;
             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 = {
         const newTarget: LocalTarget = {
             ...data,
             ...data,
             enabled: true,
             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);
+}