Explorar o código

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

Milo Schwartz hai 6 meses
pai
achega
7b3db11b82

+ 9 - 0
install/fs/docker-compose.yml

@@ -11,6 +11,7 @@ services:
       timeout: "3s"
       retries: 5
 
+{{if .InstallGerbil}}
   gerbil:
     image: fosrl/gerbil:{{.GerbilVersion}}
     container_name: gerbil
@@ -32,12 +33,20 @@ services:
       - 51820:51820/udp
       - 443:443 # Port for traefik because of the network_mode
       - 80:80 # Port for traefik because of the network_mode
+{{end}}
 
   traefik:
     image: traefik:v3.1
     container_name: traefik
     restart: unless-stopped
+{{if .InstallGerbil}}
     network_mode: service:gerbil # Ports appear on the gerbil service
+{{end}}
+{{if not .InstallGerbil}}
+    ports:
+      - 443:443
+      - 80:80
+{{end}}
     depends_on:
       pangolin:
         condition: service_healthy

+ 3 - 8
install/main.go

@@ -41,6 +41,7 @@ type Config struct {
 	EmailSMTPUser              string
 	EmailSMTPPass              string
 	EmailNoReply               string
+	InstallGerbil              bool
 }
 
 func main() {
@@ -64,7 +65,7 @@ func main() {
 		}
 
 		if !isDockerInstalled() && runtime.GOOS == "linux" {
-			if shouldInstallDocker() {
+			if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
 				installDocker()
 			}
 		}
@@ -140,6 +141,7 @@ func collectUserInput(reader *bufio.Reader) Config {
 	config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
 	config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
 	config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
+	config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunned connections", true)
 
 	// Admin user configuration
 	fmt.Println("\n=== Admin User Configuration ===")
@@ -340,13 +342,6 @@ func createConfigFiles(config Config) error {
 	return nil
 }
 
-func shouldInstallDocker() bool {
-	reader := bufio.NewReader(os.Stdin)
-	fmt.Print("Would you like to install Docker? (yes/no): ")
-	response, _ := reader.ReadString('\n')
-	return strings.ToLower(strings.TrimSpace(response)) == "yes"
-}
-
 func installDocker() error {
 	// Detect Linux distribution
 	cmd := exec.Command("cat", "/etc/os-release")

+ 51 - 23
server/routers/site/createSite.ts

@@ -24,7 +24,7 @@ const createSiteParamsSchema = z
 const createSiteSchema = z
     .object({
         name: z.string().min(1).max(255),
-        exitNodeId: z.number().int().positive(),
+        exitNodeId: z.number().int().positive().optional(),
         // subdomain: z
         //     .string()
         //     .min(1)
@@ -32,7 +32,7 @@ const createSiteSchema = z
         //     .transform((val) => val.toLowerCase())
         //     .optional(),
         pubKey: z.string().optional(),
-        subnet: z.string(),
+        subnet: z.string().optional(),
         newtId: z.string().optional(),
         secret: z.string().optional(),
         type: z.string()
@@ -82,28 +82,46 @@ export async function createSite(
 
         const niceId = await getUniqueSiteName(orgId);
 
-        let payload: any = {
-            orgId,
-            exitNodeId,
-            name,
-            niceId,
-            subnet,
-            type
-        };
-
-        if (pubKey && type == "wireguard") {
-            // we dont add the pubKey for newts because the newt will generate it
-            payload = {
-                ...payload,
-                pubKey
-            };
-        }
-
         await db.transaction(async (trx) => {
-            const [newSite] = await trx
-                .insert(sites)
-                .values(payload)
-                .returning();
+            let newSite: Site;
+
+            if (exitNodeId) {
+                // we are creating a site with an exit node (tunneled)
+                if (!subnet) {
+                    return next(
+                        createHttpError(
+                            HttpCode.BAD_REQUEST,
+                            "Subnet is required for tunneled sites"
+                        )
+                    );
+                }
+
+                [newSite] = await trx
+                    .insert(sites)
+                    .values({
+                        orgId,
+                        exitNodeId,
+                        name,
+                        niceId,
+                        subnet,
+                        type,
+                        ...(pubKey && type == "wireguard" && { pubKey })
+                    })
+                    .returning();
+            } else {
+                // we are creating a site with no tunneling
+
+                [newSite] = await trx
+                    .insert(sites)
+                    .values({
+                        orgId,
+                        name,
+                        niceId,
+                        type,
+                        subnet: "0.0.0.0/0"
+                    })
+                    .returning();
+            }
 
             const adminRole = await trx
                 .select()
@@ -149,6 +167,16 @@ export async function createSite(
                         )
                     );
                 }
+
+                if (!exitNodeId) {
+                    return next(
+                        createHttpError(
+                            HttpCode.BAD_REQUEST,
+                            "Exit node ID is required for wireguard sites"
+                        )
+                    );
+                }
+
                 await addPeer(exitNodeId, {
                     publicKey: pubKey,
                     allowedIps: []

+ 89 - 77
server/routers/target/createTarget.ts

@@ -123,88 +123,100 @@ export async function createTarget(
             );
         }
 
-        // make sure the target is within the site subnet
-        if (
-            site.type == "wireguard" &&
-            !isIpInCidr(targetData.ip, site.subnet!)
-        ) {
-            return next(
-                createHttpError(
-                    HttpCode.BAD_REQUEST,
-                    `Target IP is not within the site subnet`
-                )
-            );
-        }
-
-        // Fetch resources for this site
-        const resourcesRes = await db.query.resources.findMany({
-            where: eq(resources.siteId, site.siteId)
-        });
+        let newTarget: Target[] = [];
+        if (site.type == "local") {
+            newTarget = await db
+                .insert(targets)
+                .values({
+                    resourceId,
+                    protocol: "tcp", // hard code for now
+                    ...targetData
+                })
+                .returning();
+        } else {
+            // make sure the target is within the site subnet
+            if (
+                site.type == "wireguard" &&
+                !isIpInCidr(targetData.ip, site.subnet!)
+            ) {
+                return next(
+                    createHttpError(
+                        HttpCode.BAD_REQUEST,
+                        `Target IP is not within the site subnet`
+                    )
+                );
+            }
 
-        // TODO: is this all inefficient?
-        // Fetch targets for all resources of this site
-        let targetIps: string[] = [];
-        let targetInternalPorts: number[] = [];
-        await Promise.all(
-            resourcesRes.map(async (resource) => {
-                const targetsRes = await db.query.targets.findMany({
-                    where: eq(targets.resourceId, resource.resourceId)
-                });
-                targetsRes.forEach((target) => {
-                    targetIps.push(`${target.ip}/32`);
-                    if (target.internalPort) {
-                        targetInternalPorts.push(target.internalPort);
-                    }
-                });
-            })
-        );
+            // Fetch resources for this site
+            const resourcesRes = await db.query.resources.findMany({
+                where: eq(resources.siteId, site.siteId)
+            });
+
+            // TODO: is this all inefficient?
+            // Fetch targets for all resources of this site
+            let targetIps: string[] = [];
+            let targetInternalPorts: number[] = [];
+            await Promise.all(
+                resourcesRes.map(async (resource) => {
+                    const targetsRes = await db.query.targets.findMany({
+                        where: eq(targets.resourceId, resource.resourceId)
+                    });
+                    targetsRes.forEach((target) => {
+                        targetIps.push(`${target.ip}/32`);
+                        if (target.internalPort) {
+                            targetInternalPorts.push(target.internalPort);
+                        }
+                    });
+                })
+            );
 
-        let internalPort!: number;
-        // pick a port
-        for (let i = 40000; i < 65535; i++) {
-            if (!targetInternalPorts.includes(i)) {
-                internalPort = i;
-                break;
+            let internalPort!: number;
+            // pick a port
+            for (let i = 40000; i < 65535; i++) {
+                if (!targetInternalPorts.includes(i)) {
+                    internalPort = i;
+                    break;
+                }
             }
-        }
 
-        if (!internalPort) {
-            return next(
-                createHttpError(
-                    HttpCode.BAD_REQUEST,
-                    `No available internal port`
-                )
-            );
-        }
+            if (!internalPort) {
+                return next(
+                    createHttpError(
+                        HttpCode.BAD_REQUEST,
+                        `No available internal port`
+                    )
+                );
+            }
 
-        const newTarget = await db
-            .insert(targets)
-            .values({
-                resourceId,
-                protocol: "tcp", // hard code for now
-                internalPort,
-                ...targetData
-            })
-            .returning();
-
-        // add the new target to the targetIps array
-        targetIps.push(`${targetData.ip}/32`);
-
-        if (site.pubKey) {
-            if (site.type == "wireguard") {
-                await addPeer(site.exitNodeId!, {
-                    publicKey: site.pubKey,
-                    allowedIps: targetIps.flat()
-                });
-            } else if (site.type == "newt") {
-                // get the newt on the site by querying the newt table for siteId
-                const [newt] = await db
-                    .select()
-                    .from(newts)
-                    .where(eq(newts.siteId, site.siteId))
-                    .limit(1);
-
-                addTargets(newt.newtId, newTarget);
+            newTarget = await db
+                .insert(targets)
+                .values({
+                    resourceId,
+                    protocol: "tcp", // hard code for now
+                    internalPort,
+                    ...targetData
+                })
+                .returning();
+
+            // add the new target to the targetIps array
+            targetIps.push(`${targetData.ip}/32`);
+
+            if (site.pubKey) {
+                if (site.type == "wireguard") {
+                    await addPeer(site.exitNodeId!, {
+                        publicKey: site.pubKey,
+                        allowedIps: targetIps.flat()
+                    });
+                } else if (site.type == "newt") {
+                    // get the newt on the site by querying the newt table for siteId
+                    const [newt] = await db
+                        .select()
+                        .from(newts)
+                        .where(eq(newts.siteId, site.siteId))
+                        .limit(1);
+
+                    addTargets(newt.newtId, newTarget);
+                }
             }
         }
 

+ 138 - 64
src/app/[orgId]/settings/sites/CreateSiteForm.tsx

@@ -49,7 +49,7 @@ const createSiteFormSchema = z.object({
         .max(30, {
             message: "Name must not be longer than 30 characters."
         }),
-    method: z.enum(["wireguard", "newt"])
+    method: z.enum(["wireguard", "newt", "local"])
 });
 
 type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
@@ -79,17 +79,16 @@ export default function CreateSiteForm({
     const [isLoading, setIsLoading] = useState(false);
     const [isChecked, setIsChecked] = useState(false);
 
-    const router = useRouter();
-
     const [keypair, setKeypair] = useState<{
         publicKey: string;
         privateKey: string;
     } | null>(null);
+
     const [siteDefaults, setSiteDefaults] =
         useState<PickSiteDefaultsResponse | null>(null);
 
     const handleCheckboxChange = (checked: boolean) => {
-        setChecked?.(checked);
+        // setChecked?.(checked);
         setIsChecked(checked);
     };
 
@@ -98,6 +97,17 @@ export default function CreateSiteForm({
         defaultValues
     });
 
+    const nameField = form.watch("name");
+    const methodField = form.watch("method");
+
+    useEffect(() => {
+        const nameIsValid = nameField?.length >= 2 && nameField?.length <= 30;
+        const isFormValid = methodField === "local" || isChecked;
+
+        // Only set checked to true if name is valid AND (method is local OR checkbox is checked)
+        setChecked?.(nameIsValid && isFormValid);
+    }, [nameField, methodField, isChecked, setChecked]);
+
     useEffect(() => {
         if (!open) return;
 
@@ -114,11 +124,8 @@ export default function CreateSiteForm({
 
         api.get(`/org/${orgId}/pick-site-defaults`)
             .catch((e) => {
-                toast({
-                    variant: "destructive",
-                    title: "Error picking site defaults",
-                    description: formatAxiosError(e)
-                });
+                // update the default value of the form to be local method
+                form.setValue("method", "local");
             })
             .then((res) => {
                 if (res && res.status === 200) {
@@ -130,24 +137,54 @@ export default function CreateSiteForm({
     async function onSubmit(data: CreateSiteFormValues) {
         setLoading?.(true);
         setIsLoading(true);
-        if (!siteDefaults || !keypair) {
-            return;
-        }
         let payload: CreateSiteBody = {
             name: data.name,
-            subnet: siteDefaults.subnet,
-            exitNodeId: siteDefaults.exitNodeId,
-            pubKey: keypair.publicKey,
             type: data.method
         };
+
+        if (data.method == "wireguard") {
+            if (!keypair || !siteDefaults) {
+                toast({
+                    variant: "destructive",
+                    title: "Error creating site",
+                    description: "Key pair or site defaults not found"
+                });
+                setLoading?.(false);
+                setIsLoading(false);
+                return;
+            }
+
+            payload = {
+                ...payload,
+                subnet: siteDefaults.subnet,
+                exitNodeId: siteDefaults.exitNodeId,
+                pubKey: keypair.publicKey
+            };
+        }
         if (data.method === "newt") {
-            payload.secret = siteDefaults.newtSecret;
-            payload.newtId = siteDefaults.newtId;
+            if (!siteDefaults) {
+                toast({
+                    variant: "destructive",
+                    title: "Error creating site",
+                    description: "Site defaults not found"
+                });
+                setLoading?.(false);
+                setIsLoading(false);
+                return;
+            }
+
+            payload = {
+                ...payload,
+                secret: siteDefaults.newtSecret,
+                newtId: siteDefaults.newtId
+            };
         }
+
         const res = await api
-            .put<
-                AxiosResponse<CreateSiteResponse>
-            >(`/org/${orgId}/site/`, payload)
+            .put<AxiosResponse<CreateSiteResponse>>(
+                `/org/${orgId}/site/`,
+                payload
+            )
             .catch((e) => {
                 toast({
                     variant: "destructive",
@@ -157,18 +194,20 @@ export default function CreateSiteForm({
             });
 
         if (res && res.status === 201) {
-            const niceId = res.data.data.niceId;
-            // navigate to the site page
-            // router.push(`/${orgId}/settings/sites/${niceId}`);
-
             const data = res.data.data;
 
             onCreate?.({
                 name: data.name,
                 id: data.siteId,
                 nice: data.niceId.toString(),
-                mbIn: "0 MB",
-                mbOut: "0 MB",
+                mbIn:
+                    data.type == "wireguard" || data.type == "newt"
+                        ? "0 MB"
+                        : "--",
+                mbOut:
+                    data.type == "wireguard" || data.type == "newt"
+                        ? "0 MB"
+                        : "--",
                 orgId: orgId as string,
                 type: data.type as any,
                 online: false
@@ -245,12 +284,21 @@ PersistentKeepalive = 5`
                                             <SelectValue placeholder="Select method" />
                                         </SelectTrigger>
                                         <SelectContent>
-                                            <SelectItem value="wireguard">
-                                                WireGuard
+                                            <SelectItem value="local">
+                                                Local
                                             </SelectItem>
-                                            <SelectItem value="newt">
+                                            <SelectItem
+                                                value="newt"
+                                                disabled={!siteDefaults}
+                                            >
                                                 Newt
                                             </SelectItem>
+                                            <SelectItem
+                                                value="wireguard"
+                                                disabled={!siteDefaults}
+                                            >
+                                                WireGuard
+                                            </SelectItem>
                                         </SelectContent>
                                     </Select>
                                 </FormControl>
@@ -264,50 +312,76 @@ PersistentKeepalive = 5`
 
                     <div className="w-full">
                         {form.watch("method") === "wireguard" && !isLoading ? (
-                            <CopyTextBox text={wgConfig} />
+                            <>
+                                <CopyTextBox text={wgConfig} />
+                                <span className="text-sm text-muted-foreground">
+                                    You will only be able to see the
+                                    configuration once.
+                                </span>
+                            </>
                         ) : form.watch("method") === "wireguard" &&
                           isLoading ? (
                             <p>Loading WireGuard configuration...</p>
-                        ) : (
-                            <CopyTextBox text={newtConfig} wrapText={false} />
-                        )}
+                        ) : form.watch("method") === "newt" ? (
+                            <>
+                                <CopyTextBox
+                                    text={newtConfig}
+                                    wrapText={false}
+                                />
+                                <span className="text-sm text-muted-foreground">
+                                    You will only be able to see the
+                                    configuration once.
+                                </span>
+                            </>
+                        ) : null}
                     </div>
 
-                    <span className="text-sm text-muted-foreground">
-                        You will only be able to see the configuration once.
-                    </span>
-
                     {form.watch("method") === "newt" && (
-                        <>
-                            <br />
-                            <Link
-                                className="text-sm text-primary flex items-center gap-1"
-                                href="https://docs.fossorial.io/Newt/install"
-                                target="_blank"
-                                rel="noopener noreferrer"
-                            >
-                                <span>
-                                    {" "}
-                                    Learn how to install Newt on your system
-                                </span>
-                                <SquareArrowOutUpRight size={14} />
-                            </Link>
-                        </>
+                        <Link
+                            className="text-sm text-primary flex items-center gap-1"
+                            href="https://docs.fossorial.io/Newt/install"
+                            target="_blank"
+                            rel="noopener noreferrer"
+                        >
+                            <span>
+                                {" "}
+                                Learn how to install Newt on your system
+                            </span>
+                            <SquareArrowOutUpRight size={14} />
+                        </Link>
                     )}
 
-                    <div className="flex items-center space-x-2">
-                        <Checkbox
-                            id="terms"
-                            checked={isChecked}
-                            onCheckedChange={handleCheckboxChange}
-                        />
-                        <label
-                            htmlFor="terms"
-                            className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+                    {form.watch("method") === "local" && (
+                        <Link
+                            className="text-sm text-primary flex items-center gap-1"
+                            href="https://docs.fossorial.io/Pangolin/without-tunneling"
+                            target="_blank"
+                            rel="noopener noreferrer"
                         >
-                            I have copied the config
-                        </label>
-                    </div>
+                            <span>
+                                {" "}
+                                Local sites do not tunnel, learn more
+                            </span>
+                            <SquareArrowOutUpRight size={14} />
+                        </Link>
+                    )}
+
+                    {(form.watch("method") === "newt" ||
+                        form.watch("method") === "wireguard") && (
+                        <div className="flex items-center space-x-2">
+                            <Checkbox
+                                id="terms"
+                                checked={isChecked}
+                                onCheckedChange={handleCheckboxChange}
+                            />
+                            <label
+                                htmlFor="terms"
+                                className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
+                            >
+                                I have copied the config
+                            </label>
+                        </div>
+                    )}
                 </form>
             </Form>
         </div>

+ 29 - 15
src/app/[orgId]/settings/sites/SitesTable.tsx

@@ -23,7 +23,7 @@ import { useState } from "react";
 import CreateSiteForm from "./CreateSiteForm";
 import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
 import { useToast } from "@app/hooks/useToast";
-import { formatAxiosError } from "@app/lib/api";;
+import { formatAxiosError } from "@app/lib/api";
 import { createApiClient } from "@app/lib/api";
 import { useEnvContext } from "@app/hooks/useEnvContext";
 import CreateSiteFormModal from "./CreateSiteModal";
@@ -146,21 +146,27 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
             },
             cell: ({ row }) => {
                 const originalRow = row.original;
-
-                if (originalRow.online) {
-                    return (
-                        <span className="text-green-500 flex items-center space-x-2">
-                            <div className="w-2 h-2 bg-green-500 rounded-full"></div>
-                            <span>Online</span>
-                        </span>
-                    );
+                if (
+                    originalRow.type == "newt" ||
+                    originalRow.type == "wireguard"
+                ) {
+                    if (originalRow.online) {
+                        return (
+                            <span className="text-green-500 flex items-center space-x-2">
+                                <div className="w-2 h-2 bg-green-500 rounded-full"></div>
+                                <span>Online</span>
+                            </span>
+                        );
+                    } else {
+                        return (
+                            <span className="text-neutral-500 flex items-center space-x-2">
+                                <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
+                                <span>Offline</span>
+                            </span>
+                        );
+                    }
                 } else {
-                    return (
-                        <span className="text-neutral-500 flex items-center space-x-2">
-                            <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
-                            <span>Offline</span>
-                        </span>
-                    );
+                    return <span>--</span>;
                 }
             }
         },
@@ -245,6 +251,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
                         </div>
                     );
                 }
+
+                if (originalRow.type === "local") {
+                    return (
+                        <div className="flex items-center space-x-2">
+                            <span>Local</span>
+                        </div>
+                    );
+                }
             }
         },
         {

+ 35 - 22
src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx

@@ -16,37 +16,50 @@ type SiteInfoCardProps = {};
 export default function SiteInfoCard({}: SiteInfoCardProps) {
     const { site, updateSite } = useSiteContext();
 
+    const getConnectionTypeString = (type: string) => {
+        if (type === "newt") {
+            return "Newt";
+        } else if (type === "wireguard") {
+            return "WireGuard";
+        } else if (type === "local") {
+            return "Local";
+        } else {
+            return "Unknown";
+        }
+    };
+
     return (
         <Alert>
             <InfoIcon className="h-4 w-4" />
             <AlertTitle className="font-semibold">Site Information</AlertTitle>
             <AlertDescription className="mt-4">
                 <InfoSections>
-                    <InfoSection>
-                        <InfoSectionTitle>Status</InfoSectionTitle>
-                        <InfoSectionContent>
-                            {site.online ? (
-                                <div className="text-green-500 flex items-center space-x-2">
-                                    <div className="w-2 h-2 bg-green-500 rounded-full"></div>
-                                    <span>Online</span>
-                                </div>
-                            ) : (
-                                <div className="text-neutral-500 flex items-center space-x-2">
-                                    <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
-                                    <span>Offline</span>
-                                </div>
-                            )}
-                        </InfoSectionContent>
-                    </InfoSection>
-                    <Separator orientation="vertical" />
+                    {(site.type == "newt" || site.type == "wireguard") && (
+                        <>
+                            <InfoSection>
+                                <InfoSectionTitle>Status</InfoSectionTitle>
+                                <InfoSectionContent>
+                                    {site.online ? (
+                                        <div className="text-green-500 flex items-center space-x-2">
+                                            <div className="w-2 h-2 bg-green-500 rounded-full"></div>
+                                            <span>Online</span>
+                                        </div>
+                                    ) : (
+                                        <div className="text-neutral-500 flex items-center space-x-2">
+                                            <div className="w-2 h-2 bg-gray-500 rounded-full"></div>
+                                            <span>Offline</span>
+                                        </div>
+                                    )}
+                                </InfoSectionContent>
+                            </InfoSection>
+
+                            <Separator orientation="vertical" />
+                        </>
+                    )}
                     <InfoSection>
                         <InfoSectionTitle>Connection Type</InfoSectionTitle>
                         <InfoSectionContent>
-                            {site.type === "newt"
-                                ? "Newt"
-                                : site.type === "wireguard"
-                                  ? "WireGuard"
-                                  : "Unknown"}
+                            {getConnectionTypeString(site.type)}
                         </InfoSectionContent>
                     </InfoSection>
                 </InfoSections>

+ 6 - 3
src/app/[orgId]/settings/sites/page.tsx

@@ -23,7 +23,10 @@ export default async function SitesPage(props: SitesPageProps) {
         sites = res.data.data.sites;
     } catch (e) {}
 
-    function formatSize(mb: number): string {
+    function formatSize(mb: number, type: string): string {
+        if (type === "local") {
+            return "--"; // because we are not able to track the data use in a local site right now
+        }
         if (mb >= 1024 * 1024) {
             return `${(mb / (1024 * 1024)).toFixed(2)} TB`;
         } else if (mb >= 1024) {
@@ -38,8 +41,8 @@ export default async function SitesPage(props: SitesPageProps) {
             name: site.name,
             id: site.siteId,
             nice: site.niceId.toString(),
-            mbIn: formatSize(site.megabytesIn || 0),
-            mbOut: formatSize(site.megabytesOut || 0),
+            mbIn: formatSize(site.megabytesIn || 0, site.type),
+            mbOut: formatSize(site.megabytesOut || 0, site.type),
             orgId: params.orgId,
             type: site.type as any,
             online: site.online