mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-30 14:39:29 +02:00
Allow "local" sites witn no tunnel
This commit is contained in:
parent
5ce5fe1d19
commit
1b006b426e
4 changed files with 182 additions and 77 deletions
|
@ -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: []
|
||||
|
|
|
@ -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 === "newt") {
|
||||
payload.secret = siteDefaults.newtSecret;
|
||||
payload.newtId = siteDefaults.newtId;
|
||||
|
||||
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") {
|
||||
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,14 @@ 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 +278,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,19 +306,30 @@ 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 />
|
||||
|
@ -295,19 +348,32 @@ PersistentKeepalive = 5`
|
|||
</>
|
||||
)}
|
||||
|
||||
<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.watch("method") === "local" && (
|
||||
<>
|
||||
<br />
|
||||
<p>
|
||||
Data will leave Traefik and go wherever you
|
||||
want; no tunneling involved.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(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>
|
||||
|
|
|
@ -245,6 +245,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>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue