mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-10 04:47:11 +02:00
Merge branch 'dev' of https://github.com/fosrl/pangolin into dev
This commit is contained in:
commit
7b3db11b82
8 changed files with 359 additions and 211 deletions
|
@ -11,6 +11,7 @@ services:
|
||||||
timeout: "3s"
|
timeout: "3s"
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
{{if .InstallGerbil}}
|
||||||
gerbil:
|
gerbil:
|
||||||
image: fosrl/gerbil:{{.GerbilVersion}}
|
image: fosrl/gerbil:{{.GerbilVersion}}
|
||||||
container_name: gerbil
|
container_name: gerbil
|
||||||
|
@ -32,12 +33,20 @@ services:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 443:443 # Port for traefik because of the network_mode
|
- 443:443 # Port for traefik because of the network_mode
|
||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
{{end}}
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.1
|
image: traefik:v3.1
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
{{if .InstallGerbil}}
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
|
{{end}}
|
||||||
|
{{if not .InstallGerbil}}
|
||||||
|
ports:
|
||||||
|
- 443:443
|
||||||
|
- 80:80
|
||||||
|
{{end}}
|
||||||
depends_on:
|
depends_on:
|
||||||
pangolin:
|
pangolin:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
|
@ -41,6 +41,7 @@ type Config struct {
|
||||||
EmailSMTPUser string
|
EmailSMTPUser string
|
||||||
EmailSMTPPass string
|
EmailSMTPPass string
|
||||||
EmailNoReply string
|
EmailNoReply string
|
||||||
|
InstallGerbil bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -64,7 +65,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
||||||
if shouldInstallDocker() {
|
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
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.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.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.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
|
// Admin user configuration
|
||||||
fmt.Println("\n=== Admin User Configuration ===")
|
fmt.Println("\n=== Admin User Configuration ===")
|
||||||
|
@ -340,13 +342,6 @@ func createConfigFiles(config Config) error {
|
||||||
return nil
|
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 {
|
func installDocker() error {
|
||||||
// Detect Linux distribution
|
// Detect Linux distribution
|
||||||
cmd := exec.Command("cat", "/etc/os-release")
|
cmd := exec.Command("cat", "/etc/os-release")
|
||||||
|
|
|
@ -24,7 +24,7 @@ const createSiteParamsSchema = z
|
||||||
const createSiteSchema = z
|
const createSiteSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
exitNodeId: z.number().int().positive(),
|
exitNodeId: z.number().int().positive().optional(),
|
||||||
// subdomain: z
|
// subdomain: z
|
||||||
// .string()
|
// .string()
|
||||||
// .min(1)
|
// .min(1)
|
||||||
|
@ -32,7 +32,7 @@ const createSiteSchema = z
|
||||||
// .transform((val) => val.toLowerCase())
|
// .transform((val) => val.toLowerCase())
|
||||||
// .optional(),
|
// .optional(),
|
||||||
pubKey: z.string().optional(),
|
pubKey: z.string().optional(),
|
||||||
subnet: z.string(),
|
subnet: z.string().optional(),
|
||||||
newtId: z.string().optional(),
|
newtId: z.string().optional(),
|
||||||
secret: z.string().optional(),
|
secret: z.string().optional(),
|
||||||
type: z.string()
|
type: z.string()
|
||||||
|
@ -82,28 +82,46 @@ export async function createSite(
|
||||||
|
|
||||||
const niceId = await getUniqueSiteName(orgId);
|
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) => {
|
await db.transaction(async (trx) => {
|
||||||
const [newSite] = await trx
|
let newSite: Site;
|
||||||
.insert(sites)
|
|
||||||
.values(payload)
|
if (exitNodeId) {
|
||||||
.returning();
|
// 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
|
const adminRole = await trx
|
||||||
.select()
|
.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, {
|
await addPeer(exitNodeId, {
|
||||||
publicKey: pubKey,
|
publicKey: pubKey,
|
||||||
allowedIps: []
|
allowedIps: []
|
||||||
|
|
|
@ -123,88 +123,100 @@ export async function createTarget(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the target is within the site subnet
|
let newTarget: Target[] = [];
|
||||||
if (
|
if (site.type == "local") {
|
||||||
site.type == "wireguard" &&
|
newTarget = await db
|
||||||
!isIpInCidr(targetData.ip, site.subnet!)
|
.insert(targets)
|
||||||
) {
|
.values({
|
||||||
return next(
|
resourceId,
|
||||||
createHttpError(
|
protocol: "tcp", // hard code for now
|
||||||
HttpCode.BAD_REQUEST,
|
...targetData
|
||||||
`Target IP is not within the site subnet`
|
})
|
||||||
)
|
.returning();
|
||||||
);
|
} else {
|
||||||
}
|
// make sure the target is within the site subnet
|
||||||
|
if (
|
||||||
// Fetch resources for this site
|
site.type == "wireguard" &&
|
||||||
const resourcesRes = await db.query.resources.findMany({
|
!isIpInCidr(targetData.ip, site.subnet!)
|
||||||
where: eq(resources.siteId, site.siteId)
|
) {
|
||||||
});
|
return next(
|
||||||
|
createHttpError(
|
||||||
// TODO: is this all inefficient?
|
HttpCode.BAD_REQUEST,
|
||||||
// Fetch targets for all resources of this site
|
`Target IP is not within the site subnet`
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!internalPort) {
|
// Fetch resources for this site
|
||||||
return next(
|
const resourcesRes = await db.query.resources.findMany({
|
||||||
createHttpError(
|
where: eq(resources.siteId, site.siteId)
|
||||||
HttpCode.BAD_REQUEST,
|
});
|
||||||
`No available internal port`
|
|
||||||
)
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const newTarget = await db
|
let internalPort!: number;
|
||||||
.insert(targets)
|
// pick a port
|
||||||
.values({
|
for (let i = 40000; i < 65535; i++) {
|
||||||
resourceId,
|
if (!targetInternalPorts.includes(i)) {
|
||||||
protocol: "tcp", // hard code for now
|
internalPort = i;
|
||||||
internalPort,
|
break;
|
||||||
...targetData
|
}
|
||||||
})
|
}
|
||||||
.returning();
|
|
||||||
|
|
||||||
// add the new target to the targetIps array
|
if (!internalPort) {
|
||||||
targetIps.push(`${targetData.ip}/32`);
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
`No available internal port`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (site.pubKey) {
|
newTarget = await db
|
||||||
if (site.type == "wireguard") {
|
.insert(targets)
|
||||||
await addPeer(site.exitNodeId!, {
|
.values({
|
||||||
publicKey: site.pubKey,
|
resourceId,
|
||||||
allowedIps: targetIps.flat()
|
protocol: "tcp", // hard code for now
|
||||||
});
|
internalPort,
|
||||||
} else if (site.type == "newt") {
|
...targetData
|
||||||
// get the newt on the site by querying the newt table for siteId
|
})
|
||||||
const [newt] = await db
|
.returning();
|
||||||
.select()
|
|
||||||
.from(newts)
|
|
||||||
.where(eq(newts.siteId, site.siteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
addTargets(newt.newtId, newTarget);
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ const createSiteFormSchema = z.object({
|
||||||
.max(30, {
|
.max(30, {
|
||||||
message: "Name must not be longer than 30 characters."
|
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>;
|
type CreateSiteFormValues = z.infer<typeof createSiteFormSchema>;
|
||||||
|
@ -79,17 +79,16 @@ export default function CreateSiteForm({
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isChecked, setIsChecked] = useState(false);
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [keypair, setKeypair] = useState<{
|
const [keypair, setKeypair] = useState<{
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const [siteDefaults, setSiteDefaults] =
|
const [siteDefaults, setSiteDefaults] =
|
||||||
useState<PickSiteDefaultsResponse | null>(null);
|
useState<PickSiteDefaultsResponse | null>(null);
|
||||||
|
|
||||||
const handleCheckboxChange = (checked: boolean) => {
|
const handleCheckboxChange = (checked: boolean) => {
|
||||||
setChecked?.(checked);
|
// setChecked?.(checked);
|
||||||
setIsChecked(checked);
|
setIsChecked(checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -98,6 +97,17 @@ export default function CreateSiteForm({
|
||||||
defaultValues
|
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(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
|
|
||||||
|
@ -114,11 +124,8 @@ export default function CreateSiteForm({
|
||||||
|
|
||||||
api.get(`/org/${orgId}/pick-site-defaults`)
|
api.get(`/org/${orgId}/pick-site-defaults`)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
// update the default value of the form to be local method
|
||||||
variant: "destructive",
|
form.setValue("method", "local");
|
||||||
title: "Error picking site defaults",
|
|
||||||
description: formatAxiosError(e)
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
|
@ -130,24 +137,54 @@ export default function CreateSiteForm({
|
||||||
async function onSubmit(data: CreateSiteFormValues) {
|
async function onSubmit(data: CreateSiteFormValues) {
|
||||||
setLoading?.(true);
|
setLoading?.(true);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (!siteDefaults || !keypair) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let payload: CreateSiteBody = {
|
let payload: CreateSiteBody = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
subnet: siteDefaults.subnet,
|
|
||||||
exitNodeId: siteDefaults.exitNodeId,
|
|
||||||
pubKey: keypair.publicKey,
|
|
||||||
type: data.method
|
type: data.method
|
||||||
};
|
};
|
||||||
if (data.method === "newt") {
|
|
||||||
payload.secret = siteDefaults.newtSecret;
|
if (data.method == "wireguard") {
|
||||||
payload.newtId = siteDefaults.newtId;
|
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
|
const res = await api
|
||||||
.put<
|
.put<AxiosResponse<CreateSiteResponse>>(
|
||||||
AxiosResponse<CreateSiteResponse>
|
`/org/${orgId}/site/`,
|
||||||
>(`/org/${orgId}/site/`, payload)
|
payload
|
||||||
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
|
@ -157,18 +194,20 @@ export default function CreateSiteForm({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.status === 201) {
|
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;
|
const data = res.data.data;
|
||||||
|
|
||||||
onCreate?.({
|
onCreate?.({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
id: data.siteId,
|
id: data.siteId,
|
||||||
nice: data.niceId.toString(),
|
nice: data.niceId.toString(),
|
||||||
mbIn: "0 MB",
|
mbIn:
|
||||||
mbOut: "0 MB",
|
data.type == "wireguard" || data.type == "newt"
|
||||||
|
? "0 MB"
|
||||||
|
: "--",
|
||||||
|
mbOut:
|
||||||
|
data.type == "wireguard" || data.type == "newt"
|
||||||
|
? "0 MB"
|
||||||
|
: "--",
|
||||||
orgId: orgId as string,
|
orgId: orgId as string,
|
||||||
type: data.type as any,
|
type: data.type as any,
|
||||||
online: false
|
online: false
|
||||||
|
@ -245,12 +284,21 @@ PersistentKeepalive = 5`
|
||||||
<SelectValue placeholder="Select method" />
|
<SelectValue placeholder="Select method" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="wireguard">
|
<SelectItem value="local">
|
||||||
WireGuard
|
Local
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="newt">
|
<SelectItem
|
||||||
|
value="newt"
|
||||||
|
disabled={!siteDefaults}
|
||||||
|
>
|
||||||
Newt
|
Newt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="wireguard"
|
||||||
|
disabled={!siteDefaults}
|
||||||
|
>
|
||||||
|
WireGuard
|
||||||
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -264,50 +312,76 @@ PersistentKeepalive = 5`
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{form.watch("method") === "wireguard" && !isLoading ? (
|
{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" &&
|
) : form.watch("method") === "wireguard" &&
|
||||||
isLoading ? (
|
isLoading ? (
|
||||||
<p>Loading WireGuard configuration...</p>
|
<p>Loading WireGuard configuration...</p>
|
||||||
) : (
|
) : form.watch("method") === "newt" ? (
|
||||||
<CopyTextBox text={newtConfig} wrapText={false} />
|
<>
|
||||||
)}
|
<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>
|
</div>
|
||||||
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
You will only be able to see the configuration once.
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{form.watch("method") === "newt" && (
|
{form.watch("method") === "newt" && (
|
||||||
<>
|
<Link
|
||||||
<br />
|
className="text-sm text-primary flex items-center gap-1"
|
||||||
<Link
|
href="https://docs.fossorial.io/Newt/install"
|
||||||
className="text-sm text-primary flex items-center gap-1"
|
target="_blank"
|
||||||
href="https://docs.fossorial.io/Newt/install"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
>
|
||||||
rel="noopener noreferrer"
|
<span>
|
||||||
>
|
{" "}
|
||||||
<span>
|
Learn how to install Newt on your system
|
||||||
{" "}
|
</span>
|
||||||
Learn how to install Newt on your system
|
<SquareArrowOutUpRight size={14} />
|
||||||
</span>
|
</Link>
|
||||||
<SquareArrowOutUpRight size={14} />
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
{form.watch("method") === "local" && (
|
||||||
<Checkbox
|
<Link
|
||||||
id="terms"
|
className="text-sm text-primary flex items-center gap-1"
|
||||||
checked={isChecked}
|
href="https://docs.fossorial.io/Pangolin/without-tunneling"
|
||||||
onCheckedChange={handleCheckboxChange}
|
target="_blank"
|
||||||
/>
|
rel="noopener noreferrer"
|
||||||
<label
|
|
||||||
htmlFor="terms"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
>
|
||||||
I have copied the config
|
<span>
|
||||||
</label>
|
{" "}
|
||||||
</div>
|
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>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { useState } from "react";
|
||||||
import CreateSiteForm from "./CreateSiteForm";
|
import CreateSiteForm from "./CreateSiteForm";
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { useToast } from "@app/hooks/useToast";
|
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 { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import CreateSiteFormModal from "./CreateSiteModal";
|
import CreateSiteFormModal from "./CreateSiteModal";
|
||||||
|
@ -146,21 +146,27 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
|
if (
|
||||||
if (originalRow.online) {
|
originalRow.type == "newt" ||
|
||||||
return (
|
originalRow.type == "wireguard"
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
) {
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
if (originalRow.online) {
|
||||||
<span>Online</span>
|
return (
|
||||||
</span>
|
<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 {
|
} else {
|
||||||
return (
|
return <span>--</span>;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -245,6 +251,14 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (originalRow.type === "local") {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>Local</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,37 +16,50 @@ type SiteInfoCardProps = {};
|
||||||
export default function SiteInfoCard({}: SiteInfoCardProps) {
|
export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||||
const { site, updateSite } = useSiteContext();
|
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 (
|
return (
|
||||||
<Alert>
|
<Alert>
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle className="font-semibold">Site Information</AlertTitle>
|
<AlertTitle className="font-semibold">Site Information</AlertTitle>
|
||||||
<AlertDescription className="mt-4">
|
<AlertDescription className="mt-4">
|
||||||
<InfoSections>
|
<InfoSections>
|
||||||
<InfoSection>
|
{(site.type == "newt" || site.type == "wireguard") && (
|
||||||
<InfoSectionTitle>Status</InfoSectionTitle>
|
<>
|
||||||
<InfoSectionContent>
|
<InfoSection>
|
||||||
{site.online ? (
|
<InfoSectionTitle>Status</InfoSectionTitle>
|
||||||
<div className="text-green-500 flex items-center space-x-2">
|
<InfoSectionContent>
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
{site.online ? (
|
||||||
<span>Online</span>
|
<div className="text-green-500 flex items-center space-x-2">
|
||||||
</div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
) : (
|
<span>Online</span>
|
||||||
<div className="text-neutral-500 flex items-center space-x-2">
|
</div>
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
) : (
|
||||||
<span>Offline</span>
|
<div className="text-neutral-500 flex items-center space-x-2">
|
||||||
</div>
|
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||||
)}
|
<span>Offline</span>
|
||||||
</InfoSectionContent>
|
</div>
|
||||||
</InfoSection>
|
)}
|
||||||
<Separator orientation="vertical" />
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>Connection Type</InfoSectionTitle>
|
<InfoSectionTitle>Connection Type</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{site.type === "newt"
|
{getConnectionTypeString(site.type)}
|
||||||
? "Newt"
|
|
||||||
: site.type === "wireguard"
|
|
||||||
? "WireGuard"
|
|
||||||
: "Unknown"}
|
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
</InfoSections>
|
</InfoSections>
|
||||||
|
|
|
@ -23,7 +23,10 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||||
sites = res.data.data.sites;
|
sites = res.data.data.sites;
|
||||||
} catch (e) {}
|
} 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) {
|
if (mb >= 1024 * 1024) {
|
||||||
return `${(mb / (1024 * 1024)).toFixed(2)} TB`;
|
return `${(mb / (1024 * 1024)).toFixed(2)} TB`;
|
||||||
} else if (mb >= 1024) {
|
} else if (mb >= 1024) {
|
||||||
|
@ -38,8 +41,8 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||||
name: site.name,
|
name: site.name,
|
||||||
id: site.siteId,
|
id: site.siteId,
|
||||||
nice: site.niceId.toString(),
|
nice: site.niceId.toString(),
|
||||||
mbIn: formatSize(site.megabytesIn || 0),
|
mbIn: formatSize(site.megabytesIn || 0, site.type),
|
||||||
mbOut: formatSize(site.megabytesOut || 0),
|
mbOut: formatSize(site.megabytesOut || 0, site.type),
|
||||||
orgId: params.orgId,
|
orgId: params.orgId,
|
||||||
type: site.type as any,
|
type: site.type as any,
|
||||||
online: site.online
|
online: site.online
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue