mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-17 16:01:22 +02:00
Basic clients working
This commit is contained in:
parent
15adfcca8c
commit
28f8b05dbc
21 changed files with 387 additions and 87 deletions
|
@ -1093,7 +1093,7 @@
|
||||||
"sidebarAllUsers": "All Users",
|
"sidebarAllUsers": "All Users",
|
||||||
"sidebarIdentityProviders": "Identity Providers",
|
"sidebarIdentityProviders": "Identity Providers",
|
||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients",
|
"sidebarClients": "Clients (beta)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"enableDockerSocket": "Enable Docker Socket",
|
"enableDockerSocket": "Enable Docker Socket",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.",
|
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.",
|
||||||
|
@ -1315,5 +1315,8 @@
|
||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
||||||
"remoteSubnets": "Remote Subnets",
|
"remoteSubnets": "Remote Subnets",
|
||||||
"enterCidrRange": "Enter CIDR range",
|
"enterCidrRange": "Enter CIDR range",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24."
|
"remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.",
|
||||||
|
"resourceEnableProxy": "Enable Public Proxy",
|
||||||
|
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
||||||
|
"externalProxyEnabled": "External Proxy Enabled"
|
||||||
}
|
}
|
|
@ -94,7 +94,8 @@ export const resources = pgTable("resources", {
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
stickySession: boolean("stickySession").notNull().default(false),
|
stickySession: boolean("stickySession").notNull().default(false),
|
||||||
tlsServerName: varchar("tlsServerName"),
|
tlsServerName: varchar("tlsServerName"),
|
||||||
setHostHeader: varchar("setHostHeader")
|
setHostHeader: varchar("setHostHeader"),
|
||||||
|
enableProxy: boolean("enableProxy").notNull().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
|
|
|
@ -106,7 +106,8 @@ export const resources = sqliteTable("resources", {
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
tlsServerName: text("tlsServerName"),
|
tlsServerName: text("tlsServerName"),
|
||||||
setHostHeader: text("setHostHeader")
|
setHostHeader: text("setHostHeader"),
|
||||||
|
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
|
|
|
@ -147,7 +147,8 @@ export async function updateClient(
|
||||||
endpoint: site.endpoint,
|
endpoint: site.endpoint,
|
||||||
publicKey: site.publicKey,
|
publicKey: site.publicKey,
|
||||||
serverIP: site.address,
|
serverIP: site.address,
|
||||||
serverPort: site.listenPort
|
serverPort: site.listenPort,
|
||||||
|
remoteSubnets: site.remoteSubnets
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,16 @@ import { z } from "zod";
|
||||||
import { MessageHandler } from "../ws";
|
import { MessageHandler } from "../ws";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { db, ExitNode, exitNodes } from "@server/db";
|
import {
|
||||||
|
db,
|
||||||
|
ExitNode,
|
||||||
|
exitNodes,
|
||||||
|
resources,
|
||||||
|
Target,
|
||||||
|
targets
|
||||||
|
} from "@server/db";
|
||||||
import { clients, clientSites, Newt, sites } from "@server/db";
|
import { clients, clientSites, Newt, sites } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
import { updatePeer } from "../olm/peers";
|
import { updatePeer } from "../olm/peers";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
|
@ -191,7 +198,8 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
publicKey: site.publicKey,
|
publicKey: site.publicKey,
|
||||||
serverIP: site.address,
|
serverIP: site.address,
|
||||||
serverPort: site.listenPort
|
serverPort: site.listenPort,
|
||||||
|
remoteSubnets: site.remoteSubnets
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
@ -212,14 +220,96 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
||||||
// Filter out any null values from peers that didn't have an olm
|
// Filter out any null values from peers that didn't have an olm
|
||||||
const validPeers = peers.filter((peer) => peer !== null);
|
const validPeers = peers.filter((peer) => peer !== null);
|
||||||
|
|
||||||
|
// Improved version
|
||||||
|
const allResources = await db.transaction(async (tx) => {
|
||||||
|
// First get all resources for the site
|
||||||
|
const resourcesList = await tx
|
||||||
|
.select({
|
||||||
|
resourceId: resources.resourceId,
|
||||||
|
subdomain: resources.subdomain,
|
||||||
|
fullDomain: resources.fullDomain,
|
||||||
|
ssl: resources.ssl,
|
||||||
|
blockAccess: resources.blockAccess,
|
||||||
|
sso: resources.sso,
|
||||||
|
emailWhitelistEnabled: resources.emailWhitelistEnabled,
|
||||||
|
http: resources.http,
|
||||||
|
proxyPort: resources.proxyPort,
|
||||||
|
protocol: resources.protocol
|
||||||
|
})
|
||||||
|
.from(resources)
|
||||||
|
.where(and(eq(resources.siteId, siteId), eq(resources.http, false)));
|
||||||
|
|
||||||
|
// Get all enabled targets for these resources in a single query
|
||||||
|
const resourceIds = resourcesList.map((r) => r.resourceId);
|
||||||
|
const allTargets =
|
||||||
|
resourceIds.length > 0
|
||||||
|
? await tx
|
||||||
|
.select({
|
||||||
|
resourceId: targets.resourceId,
|
||||||
|
targetId: targets.targetId,
|
||||||
|
ip: targets.ip,
|
||||||
|
method: targets.method,
|
||||||
|
port: targets.port,
|
||||||
|
internalPort: targets.internalPort,
|
||||||
|
enabled: targets.enabled,
|
||||||
|
})
|
||||||
|
.from(targets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(targets.resourceId, resourceIds),
|
||||||
|
eq(targets.enabled, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Combine the data in JS instead of using SQL for the JSON
|
||||||
|
return resourcesList.map((resource) => ({
|
||||||
|
...resource,
|
||||||
|
targets: allTargets.filter(
|
||||||
|
(target) => target.resourceId === resource.resourceId
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const { tcpTargets, udpTargets } = allResources.reduce(
|
||||||
|
(acc, resource) => {
|
||||||
|
// Skip resources with no targets
|
||||||
|
if (!resource.targets?.length) return acc;
|
||||||
|
|
||||||
|
// Format valid targets into strings
|
||||||
|
const formattedTargets = resource.targets
|
||||||
|
.filter(
|
||||||
|
(target: Target) =>
|
||||||
|
resource.proxyPort && target?.ip && target?.port
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
(target: Target) =>
|
||||||
|
`${resource.proxyPort}:${target.ip}:${target.port}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add to the appropriate protocol array
|
||||||
|
if (resource.protocol === "tcp") {
|
||||||
|
acc.tcpTargets.push(...formattedTargets);
|
||||||
|
} else {
|
||||||
|
acc.udpTargets.push(...formattedTargets);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ tcpTargets: [] as string[], udpTargets: [] as string[] }
|
||||||
|
);
|
||||||
|
|
||||||
// Build the configuration response
|
// Build the configuration response
|
||||||
const configResponse = {
|
const configResponse = {
|
||||||
ipAddress: site.address,
|
ipAddress: site.address,
|
||||||
peers: validPeers
|
peers: validPeers,
|
||||||
|
targets: {
|
||||||
|
udp: udpTargets,
|
||||||
|
tcp: tcpTargets
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug("Sending config: ", configResponse);
|
logger.debug("Sending config: ", configResponse);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
type: "newt/wg/receive-config",
|
type: "newt/wg/receive-config",
|
||||||
|
|
|
@ -4,7 +4,8 @@ import { sendToClient } from "../ws";
|
||||||
export function addTargets(
|
export function addTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: Target[],
|
targets: Target[],
|
||||||
protocol: string
|
protocol: string,
|
||||||
|
port: number | null = null
|
||||||
) {
|
) {
|
||||||
//create a list of udp and tcp targets
|
//create a list of udp and tcp targets
|
||||||
const payloadTargets = targets.map((target) => {
|
const payloadTargets = targets.map((target) => {
|
||||||
|
@ -13,19 +14,32 @@ export function addTargets(
|
||||||
}:${target.port}`;
|
}:${target.port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = {
|
sendToClient(newtId, {
|
||||||
type: `newt/${protocol}/add`,
|
type: `newt/${protocol}/add`,
|
||||||
data: {
|
data: {
|
||||||
targets: payloadTargets
|
targets: payloadTargets
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
sendToClient(newtId, payload);
|
|
||||||
|
const payloadTargetsResources = targets.map((target) => {
|
||||||
|
return `${port ? port + ":" : ""}${
|
||||||
|
target.ip
|
||||||
|
}:${target.port}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
sendToClient(newtId, {
|
||||||
|
type: `newt/wg/${protocol}/add`,
|
||||||
|
data: {
|
||||||
|
targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeTargets(
|
export function removeTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: Target[],
|
targets: Target[],
|
||||||
protocol: string
|
protocol: string,
|
||||||
|
port: number | null = null
|
||||||
) {
|
) {
|
||||||
//create a list of udp and tcp targets
|
//create a list of udp and tcp targets
|
||||||
const payloadTargets = targets.map((target) => {
|
const payloadTargets = targets.map((target) => {
|
||||||
|
@ -34,11 +48,23 @@ export function removeTargets(
|
||||||
}:${target.port}`;
|
}:${target.port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = {
|
sendToClient(newtId, {
|
||||||
type: `newt/${protocol}/remove`,
|
type: `newt/${protocol}/remove`,
|
||||||
data: {
|
data: {
|
||||||
targets: payloadTargets
|
targets: payloadTargets
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
sendToClient(newtId, payload);
|
|
||||||
|
const payloadTargetsResources = targets.map((target) => {
|
||||||
|
return `${port ? port + ":" : ""}${
|
||||||
|
target.ip
|
||||||
|
}:${target.port}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
sendToClient(newtId, {
|
||||||
|
type: `newt/wg/${protocol}/remove`,
|
||||||
|
data: {
|
||||||
|
targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,12 +119,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
|
// if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) {
|
||||||
logger.warn(
|
// logger.warn(
|
||||||
`Site ${site.siteId} last hole punch is too old, skipping`
|
// `Site ${site.siteId} last hole punch is too old, skipping`
|
||||||
);
|
// );
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// If public key changed, delete old peer from this site
|
// If public key changed, delete old peer from this site
|
||||||
if (client.pubKey && client.pubKey != publicKey) {
|
if (client.pubKey && client.pubKey != publicKey) {
|
||||||
|
@ -175,7 +175,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
publicKey: site.publicKey,
|
publicKey: site.publicKey,
|
||||||
serverIP: site.address,
|
serverIP: site.address,
|
||||||
serverPort: site.listenPort
|
serverPort: site.listenPort,
|
||||||
|
remoteSubnets: site.remoteSubnets
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@ export async function addPeer(
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
serverIP: string | null;
|
serverIP: string | null;
|
||||||
serverPort: number | null;
|
serverPort: number | null;
|
||||||
|
remoteSubnets: string | null; // optional, comma-separated list of subnets that this site can access
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
|
@ -30,7 +31,8 @@ export async function addPeer(
|
||||||
publicKey: peer.publicKey,
|
publicKey: peer.publicKey,
|
||||||
endpoint: peer.endpoint,
|
endpoint: peer.endpoint,
|
||||||
serverIP: peer.serverIP,
|
serverIP: peer.serverIP,
|
||||||
serverPort: peer.serverPort
|
serverPort: peer.serverPort,
|
||||||
|
remoteSubnets: peer.remoteSubnets // optional, comma-separated list of subnets that this site can access
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -66,6 +68,7 @@ export async function updatePeer(
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
serverIP: string | null;
|
serverIP: string | null;
|
||||||
serverPort: number | null;
|
serverPort: number | null;
|
||||||
|
remoteSubnets?: string | null; // optional, comma-separated list of subnets that
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const [olm] = await db
|
const [olm] = await db
|
||||||
|
@ -84,7 +87,8 @@ export async function updatePeer(
|
||||||
publicKey: peer.publicKey,
|
publicKey: peer.publicKey,
|
||||||
endpoint: peer.endpoint,
|
endpoint: peer.endpoint,
|
||||||
serverIP: peer.serverIP,
|
serverIP: peer.serverIP,
|
||||||
serverPort: peer.serverPort
|
serverPort: peer.serverPort,
|
||||||
|
remoteSubnets: peer.remoteSubnets
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ const createHttpResourceSchema = z
|
||||||
siteId: z.number(),
|
siteId: z.number(),
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.enum(["tcp", "udp"]),
|
protocol: z.enum(["tcp", "udp"]),
|
||||||
domainId: z.string()
|
domainId: z.string(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine(
|
.refine(
|
||||||
|
@ -59,7 +59,8 @@ const createRawResourceSchema = z
|
||||||
siteId: z.number(),
|
siteId: z.number(),
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.enum(["tcp", "udp"]),
|
protocol: z.enum(["tcp", "udp"]),
|
||||||
proxyPort: z.number().int().min(1).max(65535)
|
proxyPort: z.number().int().min(1).max(65535),
|
||||||
|
enableProxy: z.boolean().default(true)
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine(
|
.refine(
|
||||||
|
@ -378,7 +379,7 @@ async function createRawResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, http, protocol, proxyPort } = parsedBody.data;
|
const { name, http, protocol, proxyPort, enableProxy } = parsedBody.data;
|
||||||
|
|
||||||
// if http is false check to see if there is already a resource with the same port and protocol
|
// if http is false check to see if there is already a resource with the same port and protocol
|
||||||
const existingResource = await db
|
const existingResource = await db
|
||||||
|
@ -411,7 +412,8 @@ async function createRawResource(
|
||||||
name,
|
name,
|
||||||
http,
|
http,
|
||||||
protocol,
|
protocol,
|
||||||
proxyPort
|
proxyPort,
|
||||||
|
enableProxy
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,8 @@ export async function deleteResource(
|
||||||
removeTargets(
|
removeTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
targetsToBeRemoved,
|
targetsToBeRemoved,
|
||||||
deletedResource.protocol
|
deletedResource.protocol,
|
||||||
|
deletedResource.proxyPort
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,7 +168,8 @@ export async function transferResource(
|
||||||
removeTargets(
|
removeTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
resourceTargets,
|
resourceTargets,
|
||||||
updatedResource.protocol
|
updatedResource.protocol,
|
||||||
|
updatedResource.proxyPort
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,7 +191,8 @@ export async function transferResource(
|
||||||
addTargets(
|
addTargets(
|
||||||
newt.newtId,
|
newt.newtId,
|
||||||
resourceTargets,
|
resourceTargets,
|
||||||
updatedResource.protocol
|
updatedResource.protocol,
|
||||||
|
updatedResource.proxyPort
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,8 @@ const updateRawResourceBodySchema = z
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||||
stickySession: z.boolean().optional(),
|
stickySession: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional()
|
enabled: z.boolean().optional(),
|
||||||
|
enableProxy: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
|
|
@ -173,7 +173,7 @@ export async function createTarget(
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.where(eq(newts.siteId, site.siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
addTargets(newt.newtId, newTarget, resource.protocol);
|
addTargets(newt.newtId, newTarget, resource.protocol, resource.proxyPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,7 @@ export async function deleteTarget(
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.where(eq(newts.siteId, site.siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
removeTargets(newt.newtId, [deletedTarget], resource.protocol);
|
removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ export async function updateTarget(
|
||||||
.where(eq(newts.siteId, site.siteId))
|
.where(eq(newts.siteId, site.siteId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
addTargets(newt.newtId, [updatedTarget], resource.protocol);
|
addTargets(newt.newtId, [updatedTarget], resource.protocol, resource.proxyPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|
|
@ -66,7 +66,8 @@ export async function traefikConfigProvider(
|
||||||
enabled: resources.enabled,
|
enabled: resources.enabled,
|
||||||
stickySession: resources.stickySession,
|
stickySession: resources.stickySession,
|
||||||
tlsServerName: resources.tlsServerName,
|
tlsServerName: resources.tlsServerName,
|
||||||
setHostHeader: resources.setHostHeader
|
setHostHeader: resources.setHostHeader,
|
||||||
|
enableProxy: resources.enableProxy
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.innerJoin(sites, eq(sites.siteId, resources.siteId))
|
.innerJoin(sites, eq(sites.siteId, resources.siteId))
|
||||||
|
@ -365,6 +366,10 @@ export async function traefikConfigProvider(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-HTTP (TCP/UDP) configuration
|
// Non-HTTP (TCP/UDP) configuration
|
||||||
|
if (!resource.enableProxy) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const protocol = resource.protocol.toLowerCase();
|
const protocol = resource.protocol.toLowerCase();
|
||||||
const port = resource.proxyPort;
|
const port = resource.proxyPort;
|
||||||
|
|
||||||
|
|
29
server/setup/scriptsSqlite/1.8.0.ts
Normal file
29
server/setup/scriptsSqlite/1.8.0.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { APP_PATH } from "@server/lib/consts";
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const version = "1.8.0";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log("Running setup script ${version}...");
|
||||||
|
|
||||||
|
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
const db = new Database(location);
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction(() => {
|
||||||
|
db.exec(`
|
||||||
|
ALTER TABLE 'user' ADD 'termsAcceptedTimestamp' text;
|
||||||
|
ALTER TABLE 'user' ADD 'termsVersion' text;
|
||||||
|
ALTER TABLE 'sites' ADD 'remoteSubnets' text;
|
||||||
|
`);
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log("Migrated database schema");
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Unable to migrate database schema");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${version} migration complete`);
|
||||||
|
}
|
|
@ -48,7 +48,7 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
title="Manage Clients"
|
title="Manage Clients (beta)"
|
||||||
description="Clients are devices that can connect to your sites"
|
description="Clients are devices that can connect to your sites"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { useEffect, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RotateCw } from "lucide-react";
|
import { RotateCw } from "lucide-react";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
type ResourceInfoBoxType = {};
|
type ResourceInfoBoxType = {};
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
<Alert>
|
<Alert>
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle className="font-semibold">
|
<AlertTitle className="font-semibold">
|
||||||
{t('resourceInfo')}
|
{t("resourceInfo")}
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription className="mt-4">
|
<AlertDescription className="mt-4">
|
||||||
<InfoSections cols={4}>
|
<InfoSections cols={4}>
|
||||||
|
@ -42,7 +43,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
<>
|
<>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
{t('authentication')}
|
{t("authentication")}
|
||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{authInfo.password ||
|
{authInfo.password ||
|
||||||
|
@ -51,12 +52,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
authInfo.whitelist ? (
|
authInfo.whitelist ? (
|
||||||
<div className="flex items-start space-x-2 text-green-500">
|
<div className="flex items-start space-x-2 text-green-500">
|
||||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||||
<span>{t('protected')}</span>
|
<span>{t("protected")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center space-x-2 text-yellow-500">
|
<div className="flex items-center space-x-2 text-yellow-500">
|
||||||
<ShieldOff className="w-4 h-4" />
|
<ShieldOff className="w-4 h-4" />
|
||||||
<span>{t('notProtected')}</span>
|
<span>{t("notProtected")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
|
@ -71,7 +72,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t('site')}</InfoSectionTitle>
|
<InfoSectionTitle>{t("site")}</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{resource.siteName}
|
{resource.siteName}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
|
@ -98,7 +99,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t('protocol')}</InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
|
{t("protocol")}
|
||||||
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<span>
|
<span>
|
||||||
{resource.protocol.toUpperCase()}
|
{resource.protocol.toUpperCase()}
|
||||||
|
@ -106,7 +109,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t('port')}</InfoSectionTitle>
|
<InfoSectionTitle>{t("port")}</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<CopyToClipboard
|
<CopyToClipboard
|
||||||
text={resource.proxyPort!.toString()}
|
text={resource.proxyPort!.toString()}
|
||||||
|
@ -114,13 +117,29 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
/>
|
/>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
|
{build == "oss" && (
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>
|
||||||
|
{t("externalProxyEnabled")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
<span>
|
||||||
|
{resource.enableProxy
|
||||||
|
? t("enabled")
|
||||||
|
: t("disabled")}
|
||||||
|
</span>
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t('visibility')}</InfoSectionTitle>
|
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<span>
|
<span>
|
||||||
{resource.enabled ? t('enabled') : t('disabled')}
|
{resource.enabled
|
||||||
|
? t("enabled")
|
||||||
|
: t("disabled")}
|
||||||
</span>
|
</span>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
|
|
|
@ -66,6 +66,7 @@ import {
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Credenza,
|
Credenza,
|
||||||
CredenzaBody,
|
CredenzaBody,
|
||||||
|
@ -78,6 +79,7 @@ import {
|
||||||
} from "@app/components/Credenza";
|
} from "@app/components/Credenza";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
import { Globe } from "lucide-react";
|
import { Globe } from "lucide-react";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const TransferFormSchema = z.object({
|
const TransferFormSchema = z.object({
|
||||||
siteId: z.number()
|
siteId: z.number()
|
||||||
|
@ -118,25 +120,31 @@ export default function GeneralForm() {
|
||||||
fullDomain: string;
|
fullDomain: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z
|
||||||
enabled: z.boolean(),
|
.object({
|
||||||
subdomain: z.string().optional(),
|
enabled: z.boolean(),
|
||||||
name: z.string().min(1).max(255),
|
subdomain: z.string().optional(),
|
||||||
domainId: z.string().optional(),
|
name: z.string().min(1).max(255),
|
||||||
proxyPort: z.number().int().min(1).max(65535).optional()
|
domainId: z.string().optional(),
|
||||||
}).refine((data) => {
|
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||||
// For non-HTTP resources, proxyPort should be defined
|
enableProxy: z.boolean().optional()
|
||||||
if (!resource.http) {
|
})
|
||||||
return data.proxyPort !== undefined;
|
.refine(
|
||||||
}
|
(data) => {
|
||||||
// For HTTP resources, proxyPort should be undefined
|
// For non-HTTP resources, proxyPort should be defined
|
||||||
return data.proxyPort === undefined;
|
if (!resource.http) {
|
||||||
}, {
|
return data.proxyPort !== undefined;
|
||||||
message: !resource.http
|
}
|
||||||
? "Port number is required for non-HTTP resources"
|
// For HTTP resources, proxyPort should be undefined
|
||||||
: "Port number should not be set for HTTP resources",
|
return data.proxyPort === undefined;
|
||||||
path: ["proxyPort"]
|
},
|
||||||
});
|
{
|
||||||
|
message: !resource.http
|
||||||
|
? "Port number is required for non-HTTP resources"
|
||||||
|
: "Port number should not be set for HTTP resources",
|
||||||
|
path: ["proxyPort"]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
|
||||||
|
@ -147,7 +155,8 @@ export default function GeneralForm() {
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||||
domainId: resource.domainId || undefined,
|
domainId: resource.domainId || undefined,
|
||||||
proxyPort: resource.proxyPort || undefined
|
proxyPort: resource.proxyPort || undefined,
|
||||||
|
enableProxy: resource.enableProxy || false
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
|
@ -211,7 +220,8 @@ export default function GeneralForm() {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain,
|
||||||
domainId: data.domainId,
|
domainId: data.domainId,
|
||||||
proxyPort: data.proxyPort
|
proxyPort: data.proxyPort,
|
||||||
|
enableProxy: data.enableProxy
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -238,7 +248,8 @@ export default function GeneralForm() {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain,
|
||||||
fullDomain: resource.fullDomain,
|
fullDomain: resource.fullDomain,
|
||||||
proxyPort: data.proxyPort
|
proxyPort: data.proxyPort,
|
||||||
|
enableProxy: data.enableProxy
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
@ -357,16 +368,29 @@ export default function GeneralForm() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("resourcePortNumber")}
|
{t(
|
||||||
|
"resourcePortNumber"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={field.value ?? ""}
|
value={
|
||||||
onChange={(e) =>
|
field.value ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
field.onChange(
|
field.onChange(
|
||||||
e.target.value
|
e
|
||||||
? parseInt(e.target.value)
|
.target
|
||||||
|
.value
|
||||||
|
? parseInt(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
: undefined
|
: undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -374,11 +398,49 @@ export default function GeneralForm() {
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("resourcePortNumberDescription")}
|
{t(
|
||||||
|
"resourcePortNumberDescription"
|
||||||
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{build == "oss" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="enableProxy"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
variant={
|
||||||
|
"outlinePrimarySquare"
|
||||||
|
}
|
||||||
|
checked={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onCheckedChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"resourceEnableProxy"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"resourceEnableProxyDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Input } from "@app/components/ui/input";
|
import { Input } from "@app/components/ui/input";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { ListSitesResponse } from "@server/routers/site";
|
import { ListSitesResponse } from "@server/routers/site";
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
|
@ -64,6 +65,7 @@ import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import DomainPicker from "@app/components/DomainPicker";
|
import DomainPicker from "@app/components/DomainPicker";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
const baseResourceFormSchema = z.object({
|
const baseResourceFormSchema = z.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
|
@ -78,7 +80,8 @@ const httpResourceFormSchema = z.object({
|
||||||
|
|
||||||
const tcpUdpResourceFormSchema = z.object({
|
const tcpUdpResourceFormSchema = z.object({
|
||||||
protocol: z.string(),
|
protocol: z.string(),
|
||||||
proxyPort: z.number().int().min(1).max(65535)
|
proxyPort: z.number().int().min(1).max(65535),
|
||||||
|
enableProxy: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
|
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
|
||||||
|
@ -144,7 +147,8 @@ export default function Page() {
|
||||||
resolver: zodResolver(tcpUdpResourceFormSchema),
|
resolver: zodResolver(tcpUdpResourceFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
protocol: "tcp",
|
protocol: "tcp",
|
||||||
proxyPort: undefined
|
proxyPort: undefined,
|
||||||
|
enableProxy: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -163,16 +167,17 @@ export default function Page() {
|
||||||
|
|
||||||
if (isHttp) {
|
if (isHttp) {
|
||||||
const httpData = httpForm.getValues();
|
const httpData = httpForm.getValues();
|
||||||
Object.assign(payload, {
|
Object.assign(payload, {
|
||||||
subdomain: httpData.subdomain,
|
subdomain: httpData.subdomain,
|
||||||
domainId: httpData.domainId,
|
domainId: httpData.domainId,
|
||||||
protocol: "tcp",
|
protocol: "tcp"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const tcpUdpData = tcpUdpForm.getValues();
|
const tcpUdpData = tcpUdpForm.getValues();
|
||||||
Object.assign(payload, {
|
Object.assign(payload, {
|
||||||
protocol: tcpUdpData.protocol,
|
protocol: tcpUdpData.protocol,
|
||||||
proxyPort: tcpUdpData.proxyPort
|
proxyPort: tcpUdpData.proxyPort,
|
||||||
|
enableProxy: tcpUdpData.enableProxy
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,8 +203,15 @@ export default function Page() {
|
||||||
if (isHttp) {
|
if (isHttp) {
|
||||||
router.push(`/${orgId}/settings/resources/${id}`);
|
router.push(`/${orgId}/settings/resources/${id}`);
|
||||||
} else {
|
} else {
|
||||||
setShowSnippets(true);
|
const tcpUdpData = tcpUdpForm.getValues();
|
||||||
router.refresh();
|
// Only show config snippets if enableProxy is explicitly true
|
||||||
|
if (tcpUdpData.enableProxy === true) {
|
||||||
|
setShowSnippets(true);
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
// If enableProxy is false or undefined, go directly to resource page
|
||||||
|
router.push(`/${orgId}/settings/resources/${id}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -603,6 +615,46 @@ export default function Page() {
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{build == "oss" && (
|
||||||
|
<FormField
|
||||||
|
control={
|
||||||
|
tcpUdpForm.control
|
||||||
|
}
|
||||||
|
name="enableProxy"
|
||||||
|
render={({
|
||||||
|
field
|
||||||
|
}) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
variant={
|
||||||
|
"outlinePrimarySquare"
|
||||||
|
}
|
||||||
|
checked={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onCheckedChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"resourceEnableProxy"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"resourceEnableProxyDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue