mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-13 23:45:00 +02:00
Squashed commit of the following:
commit c276d2193da5dbe7af5197bdf7e2bcce6f87b0cf Author: Owen Schwartz <owen@txv.io> Date: Tue Jan 28 22:06:04 2025 -0500 Okay actually now commit 9afdc0aadc3f4fb4e811930bacff70a9e17eab9f Author: Owen Schwartz <owen@txv.io> Date: Tue Jan 28 21:58:44 2025 -0500 Migrations working finally commit a7336b3b2466fe74d650b9c253ecadbe1eff749d Merge: e7c7203fdb1ab4
Author: Owen Schwartz <owen@txv.io> Date: Mon Jan 27 22:19:15 2025 -0500 Merge branch 'dev' into tcp-udp-traffic commit e7c7203330b1b08e570048b10ef314b55068e466 Author: Owen Schwartz <owen@txv.io> Date: Mon Jan 27 22:18:09 2025 -0500 Working on migration commit a4704dfd44b10647257c7c7054c0dae806d315bb Author: Owen Schwartz <owen@txv.io> Date: Mon Jan 27 21:40:52 2025 -0500 Add flag to allow raw resources commit d74f7a57ed11e2a6bf1a7e0c28c29fb07eb573a0 Merge: 6817788 d791b9b Author: Owen Schwartz <owen@txv.io> Date: Mon Jan 27 21:28:50 2025 -0500 Merge branch 'tcp-udp-traffic' of https://github.com/fosrl/pangolin into tcp-udp-traffic commit 68177882781b54ef30b62cca7dee8bbed7c5a2fa Author: Owen Schwartz <owen@txv.io> Date: Mon Jan 27 21:28:32 2025 -0500 Get everything working commit d791b9b47f9f6ca050d6edfd1d674438f8562d99 Author: Milo Schwartz <mschwartz10612@gmail.com> Date: Mon Jan 27 17:46:19 2025 -0500 fix orgId check in verifyAdmin commit 6ac30afd7a449a126190d311bd98d7f1048f73a4 Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 23:19:33 2025 -0500 Trying to figure out traefik... commit 9886b42272882f8bb6baff2efdbe26cee7cac2b6 Merge: 786e67e 85e9129 Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 21:53:32 2025 -0500 Merge branch 'tcp-udp-traffic' of https://github.com/fosrl/pangolin into tcp-udp-traffic commit 786e67eadd6df1ee8df24e77aed20c1f1fc9ca67 Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 21:51:37 2025 -0500 Bug fixing commit 85e9129ae313b2e4a460a8bc53a0af9f9fbbafb2 Author: Milo Schwartz <mschwartz10612@gmail.com> Date: Sun Jan 26 18:35:24 2025 -0500 rethrow errors in migration and remove permanent redirect commit bd82699505fc7510c27f72cd80ea0ce815d8c5ef Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 17:49:12 2025 -0500 Fix merge issue commit 933dbf3a02b1f19fd1f627410b2407fdf05cd9bf Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 17:46:13 2025 -0500 Add sql to update resources and targets commit f19437bad847c8dbf57fddd2c48cd17bab20ddb0 Merge: 58980eb9f1f291
Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 17:19:51 2025 -0500 Merge branch 'dev' into tcp-udp-traffic commit 58980ebb64d1040b4d224c76beb38c2254f3c5d9 Merge: 1de682ad284d36
Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 17:10:09 2025 -0500 Merge branch 'dev' into tcp-udp-traffic commit 1de682a9f6039f40e05c8901c7381a94b0d018ed Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 17:08:29 2025 -0500 Working on migrations commit dc853d2bc02b11997be5c3c7ea789402716fb4c2 Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 16:56:49 2025 -0500 Finish config of resource pages commit 37c681c08d7ab73d2cad41e7ef1dbe3a8852e1f2 Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 16:07:25 2025 -0500 Finish up table commit 461c6650bbea0d7439cc042971ec13fdb52a7431 Author: Owen Schwartz <owen@txv.io> Date: Sun Jan 26 15:54:46 2025 -0500 Working toward having dual resource types commit f0894663627375e16ce6994370cb30b298efc2dc Author: Owen Schwartz <owen@txv.io> Date: Sat Jan 25 22:31:25 2025 -0500 Add qutoes commit edc535b79b94c2e65b290cd90a69fe17d27245e9 Author: Owen Schwartz <owen@txv.io> Date: Sat Jan 25 22:28:45 2025 -0500 Add readTimeout to allow long file uploads commit 194892fa14b505bd7c2b31873dc13d4b8996c0e1 Author: Owen Schwartz <owen@txv.io> Date: Sat Jan 25 20:37:34 2025 -0500 Rework traefik config generation commit ad3f896b5333e4706d610c3198f29dcd67610365 Author: Owen Schwartz <owen@txv.io> Date: Sat Jan 25 13:01:47 2025 -0500 Add proxy port to api commit ca6013b2ffda0924a696ec3141825a54a4e5297d Author: Owen Schwartz <owen@txv.io> Date: Sat Jan 25 12:58:01 2025 -0500 Add migration commit 2258d76cb3a49d3db7f05f76d8b8a9f1c248b5e4 Author: Owen Schwartz <owen@txv.io> Date: Sat Jan 25 12:55:02 2025 -0500 Add new proxy port
This commit is contained in:
parent
f874449d36
commit
0e04e82b88
32 changed files with 1003 additions and 451 deletions
|
@ -41,3 +41,4 @@ flags:
|
|||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: true
|
||||
allow_raw_resources: true
|
|
@ -33,6 +33,9 @@ entryPoints:
|
|||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
|
|
|
@ -54,3 +54,4 @@ flags:
|
|||
require_email_verification: {{.EnableEmail}}
|
||||
disable_signup_without_invite: {{.DisableSignupWithoutInvite}}
|
||||
disable_user_create_org: {{.DisableUserCreateOrg}}
|
||||
allow_raw_resources: true
|
||||
|
|
|
@ -4,7 +4,13 @@ api:
|
|||
|
||||
providers:
|
||||
http:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/http"
|
||||
pollInterval: "5s"
|
||||
udp:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/udp"
|
||||
pollInterval: "5s"
|
||||
tcp:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/tcp"
|
||||
pollInterval: "5s"
|
||||
file:
|
||||
filename: "/etc/traefik/dynamic_config.yml"
|
||||
|
@ -33,6 +39,9 @@ entryPoints:
|
|||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
|
|
|
@ -41,13 +41,16 @@ export const resources = sqliteTable("resources", {
|
|||
})
|
||||
.notNull(),
|
||||
name: text("name").notNull(),
|
||||
subdomain: text("subdomain").notNull(),
|
||||
fullDomain: text("fullDomain").notNull().unique(),
|
||||
subdomain: text("subdomain"),
|
||||
fullDomain: text("fullDomain"),
|
||||
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
||||
blockAccess: integer("blockAccess", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false),
|
||||
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
|
||||
http: integer("http", { mode: "boolean" }).notNull().default(true),
|
||||
protocol: text("protocol").notNull(),
|
||||
proxyPort: integer("proxyPort"),
|
||||
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
||||
.notNull()
|
||||
.default(false)
|
||||
|
@ -61,10 +64,9 @@ export const targets = sqliteTable("targets", {
|
|||
})
|
||||
.notNull(),
|
||||
ip: text("ip").notNull(),
|
||||
method: text("method").notNull(),
|
||||
method: text("method"),
|
||||
port: integer("port").notNull(),
|
||||
internalPort: integer("internalPort"),
|
||||
protocol: text("protocol"),
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
|
||||
});
|
||||
|
||||
|
|
|
@ -151,7 +151,8 @@ const configSchema = z.object({
|
|||
.object({
|
||||
require_email_verification: z.boolean().optional(),
|
||||
disable_signup_without_invite: z.boolean().optional(),
|
||||
disable_user_create_org: z.boolean().optional()
|
||||
disable_user_create_org: z.boolean().optional(),
|
||||
allow_raw_resources: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
@ -254,6 +255,10 @@ export class Config {
|
|||
?.require_email_verification
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
|
||||
?.allow_raw_resources
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.session_cookie_name;
|
||||
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
||||
|
|
|
@ -13,7 +13,7 @@ export async function verifyAdmin(
|
|||
const userId = req.user?.userId;
|
||||
const orgId = req.userOrgId;
|
||||
|
||||
if (!userId) {
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.UNAUTHORIZED, "User does not have orgId")
|
||||
);
|
||||
|
|
|
@ -163,7 +163,7 @@ export async function exchangeSession(
|
|||
const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
|
||||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
resource.fullDomain,
|
||||
resource.fullDomain!,
|
||||
token,
|
||||
!resource.ssl
|
||||
);
|
||||
|
|
|
@ -352,7 +352,7 @@ async function createAccessTokenSession(
|
|||
const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
|
||||
const cookie = serializeResourceSessionCookie(
|
||||
cookieName,
|
||||
resource.fullDomain,
|
||||
resource.fullDomain!,
|
||||
token,
|
||||
!resource.ssl
|
||||
);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Router } from "express";
|
||||
import * as gerbil from "@server/routers/gerbil";
|
||||
import * as badger from "@server/routers/badger";
|
||||
import * as traefik from "@server/routers/traefik";
|
||||
import * as auth from "@server/routers/auth";
|
||||
import * as resource from "@server/routers/resource";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares";
|
||||
import { getExchangeToken } from "./resource/getExchangeToken";
|
||||
import { verifyResourceSession } from "./badger";
|
||||
import { exchangeSession } from "./badger/exchangeSession";
|
||||
|
||||
// Root routes
|
||||
const internalRouter = Router();
|
||||
|
@ -15,6 +16,7 @@ internalRouter.get("/", (_, res) => {
|
|||
});
|
||||
|
||||
internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
|
||||
|
||||
internalRouter.get(
|
||||
"/resource-session/:resourceId/:token",
|
||||
auth.checkResourceSession
|
||||
|
@ -24,7 +26,7 @@ internalRouter.post(
|
|||
`/resource/:resourceId/get-exchange-token`,
|
||||
verifySessionUserMiddleware,
|
||||
verifyResourceAccess,
|
||||
resource.getExchangeToken
|
||||
getExchangeToken
|
||||
);
|
||||
|
||||
// Gerbil routes
|
||||
|
@ -38,7 +40,7 @@ gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
|
|||
const badgerRouter = Router();
|
||||
internalRouter.use("/badger", badgerRouter);
|
||||
|
||||
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
||||
badgerRouter.post("/exchange-session", badger.exchangeSession);
|
||||
badgerRouter.post("/verify-session", verifyResourceSession);
|
||||
badgerRouter.post("/exchange-session", exchangeSession);
|
||||
|
||||
export default internalRouter;
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import db from "@server/db";
|
||||
import { MessageHandler } from "../ws";
|
||||
import { exitNodes, resources, sites, targets } from "@server/db/schema";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import {
|
||||
exitNodes,
|
||||
resources,
|
||||
sites,
|
||||
Target,
|
||||
targets
|
||||
} from "@server/db/schema";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import { addPeer, deletePeer } from "../gerbil/peers";
|
||||
import logger from "@server/logger";
|
||||
|
||||
|
@ -69,37 +75,67 @@ export const handleRegisterMessage: MessageHandler = async (context) => {
|
|||
allowedIps: [site.subnet]
|
||||
});
|
||||
|
||||
const siteResources = await db
|
||||
.select()
|
||||
const allResources = await db
|
||||
.select({
|
||||
// Resource fields
|
||||
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,
|
||||
// Targets as a subquery
|
||||
targets: sql<string>`json_group_array(json_object(
|
||||
'targetId', ${targets.targetId},
|
||||
'ip', ${targets.ip},
|
||||
'method', ${targets.method},
|
||||
'port', ${targets.port},
|
||||
'internalPort', ${targets.internalPort},
|
||||
'enabled', ${targets.enabled}
|
||||
))`.as("targets")
|
||||
})
|
||||
.from(resources)
|
||||
.where(eq(resources.siteId, siteId));
|
||||
|
||||
// get the targets from the resourceIds
|
||||
const siteTargets = await db
|
||||
.select()
|
||||
.from(targets)
|
||||
.where(
|
||||
inArray(
|
||||
targets.resourceId,
|
||||
siteResources.map((resource) => resource.resourceId)
|
||||
.leftJoin(
|
||||
targets,
|
||||
and(
|
||||
eq(targets.resourceId, resources.resourceId),
|
||||
eq(targets.enabled, true)
|
||||
)
|
||||
);
|
||||
)
|
||||
.groupBy(resources.resourceId);
|
||||
|
||||
const udpTargets = siteTargets
|
||||
.filter((target) => target.protocol === "udp")
|
||||
.map((target) => {
|
||||
return `${target.internalPort ? target.internalPort + ":" : ""}${
|
||||
target.ip
|
||||
}:${target.port}`;
|
||||
});
|
||||
let tcpTargets: string[] = [];
|
||||
let udpTargets: string[] = [];
|
||||
|
||||
const tcpTargets = siteTargets
|
||||
.filter((target) => target.protocol === "tcp")
|
||||
.map((target) => {
|
||||
return `${target.internalPort ? target.internalPort + ":" : ""}${
|
||||
target.ip
|
||||
}:${target.port}`;
|
||||
});
|
||||
for (const resource of allResources) {
|
||||
const targets = JSON.parse(resource.targets);
|
||||
if (!targets || targets.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (resource.protocol === "tcp") {
|
||||
tcpTargets = tcpTargets.concat(
|
||||
targets.map(
|
||||
(target: Target) =>
|
||||
`${
|
||||
target.internalPort ? target.internalPort + ":" : ""
|
||||
}${target.ip}:${target.port}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
udpTargets = tcpTargets.concat(
|
||||
targets.map(
|
||||
(target: Target) =>
|
||||
`${
|
||||
target.internalPort ? target.internalPort + ":" : ""
|
||||
}${target.ip}:${target.port}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: {
|
||||
|
|
|
@ -1,73 +1,44 @@
|
|||
import { Target } from "@server/db/schema";
|
||||
import { sendToClient } from "../ws";
|
||||
|
||||
export async function addTargets(newtId: string, targets: Target[]): Promise<void> {
|
||||
export async function addTargets(
|
||||
newtId: string,
|
||||
targets: Target[],
|
||||
protocol: string
|
||||
): Promise<void> {
|
||||
//create a list of udp and tcp targets
|
||||
const udpTargets = targets
|
||||
.filter((target) => target.protocol === "udp")
|
||||
.map((target) => {
|
||||
return `${target.internalPort ? target.internalPort + ":" : ""}${target.ip}:${target.port}`;
|
||||
});
|
||||
const payloadTargets = targets.map((target) => {
|
||||
return `${target.internalPort ? target.internalPort + ":" : ""}${
|
||||
target.ip
|
||||
}:${target.port}`;
|
||||
});
|
||||
|
||||
const tcpTargets = targets
|
||||
.filter((target) => target.protocol === "tcp")
|
||||
.map((target) => {
|
||||
return `${target.internalPort ? target.internalPort + ":" : ""}${target.ip}:${target.port}`;
|
||||
});
|
||||
|
||||
if (udpTargets.length > 0) {
|
||||
const payload = {
|
||||
type: `newt/udp/add`,
|
||||
data: {
|
||||
targets: udpTargets,
|
||||
},
|
||||
};
|
||||
sendToClient(newtId, payload);
|
||||
}
|
||||
|
||||
if (tcpTargets.length > 0) {
|
||||
const payload = {
|
||||
type: `newt/tcp/add`,
|
||||
data: {
|
||||
targets: tcpTargets,
|
||||
},
|
||||
};
|
||||
sendToClient(newtId, payload);
|
||||
}
|
||||
const payload = {
|
||||
type: `newt/${protocol}/add`,
|
||||
data: {
|
||||
targets: payloadTargets
|
||||
}
|
||||
};
|
||||
sendToClient(newtId, payload);
|
||||
}
|
||||
|
||||
|
||||
export async function removeTargets(newtId: string, targets: Target[]): Promise<void> {
|
||||
export async function removeTargets(
|
||||
newtId: string,
|
||||
targets: Target[],
|
||||
protocol: string
|
||||
): Promise<void> {
|
||||
//create a list of udp and tcp targets
|
||||
const udpTargets = targets
|
||||
.filter((target) => target.protocol === "udp")
|
||||
.map((target) => {
|
||||
return `${target.internalPort ? target.internalPort + ":" : ""}${target.ip}:${target.port}`;
|
||||
});
|
||||
const payloadTargets = targets.map((target) => {
|
||||
return `${target.internalPort ? target.internalPort + ":" : ""}${
|
||||
target.ip
|
||||
}:${target.port}`;
|
||||
});
|
||||
|
||||
const tcpTargets = targets
|
||||
.filter((target) => target.protocol === "tcp")
|
||||
.map((target) => {
|
||||
return `${target.internalPort ? target.internalPort + ":" : ""}${target.ip}:${target.port}`;
|
||||
});
|
||||
|
||||
if (udpTargets.length > 0) {
|
||||
const payload = {
|
||||
type: `newt/udp/remove`,
|
||||
data: {
|
||||
targets: udpTargets,
|
||||
},
|
||||
};
|
||||
sendToClient(newtId, payload);
|
||||
}
|
||||
|
||||
if (tcpTargets.length > 0) {
|
||||
const payload = {
|
||||
type: `newt/tcp/remove`,
|
||||
data: {
|
||||
targets: tcpTargets,
|
||||
},
|
||||
};
|
||||
sendToClient(newtId, payload);
|
||||
}
|
||||
const payload = {
|
||||
type: `newt/${protocol}/remove`,
|
||||
data: {
|
||||
targets: payloadTargets
|
||||
}
|
||||
};
|
||||
sendToClient(newtId, payload);
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import createHttpError from "http-errors";
|
|||
import { eq, and } from "drizzle-orm";
|
||||
import stoi from "@server/lib/stoi";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||
import logger from "@server/logger";
|
||||
|
||||
const createResourceParamsSchema = z
|
||||
|
@ -28,10 +27,35 @@ const createResourceParamsSchema = z
|
|||
|
||||
const createResourceSchema = z
|
||||
.object({
|
||||
subdomain: z
|
||||
.union([
|
||||
z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
|
||||
"Invalid subdomain format"
|
||||
)
|
||||
.min(1, "Subdomain must be at least 1 character long")
|
||||
.transform((val) => val.toLowerCase()),
|
||||
z.string().optional()
|
||||
])
|
||||
.optional(),
|
||||
name: z.string().min(1).max(255),
|
||||
subdomain: subdomainSchema
|
||||
})
|
||||
.strict();
|
||||
http: z.boolean(),
|
||||
protocol: z.string(),
|
||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.http === true) {
|
||||
return true;
|
||||
}
|
||||
return !!data.proxyPort;
|
||||
},
|
||||
{
|
||||
message: "Port number is required for non-HTTP resources",
|
||||
path: ["proxyPort"]
|
||||
}
|
||||
);
|
||||
|
||||
export type CreateResourceResponse = Resource;
|
||||
|
||||
|
@ -51,7 +75,7 @@ export async function createResource(
|
|||
);
|
||||
}
|
||||
|
||||
let { name, subdomain } = parsedBody.data;
|
||||
let { name, subdomain, protocol, proxyPort, http } = parsedBody.data;
|
||||
|
||||
// Validate request params
|
||||
const parsedParams = createResourceParamsSchema.safeParse(req.params);
|
||||
|
@ -89,15 +113,65 @@ export async function createResource(
|
|||
}
|
||||
|
||||
const fullDomain = `${subdomain}.${org[0].domain}`;
|
||||
// if http is false check to see if there is already a resource with the same port and protocol
|
||||
if (!http) {
|
||||
const existingResource = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(
|
||||
and(
|
||||
eq(resources.protocol, protocol),
|
||||
eq(resources.proxyPort, proxyPort!)
|
||||
)
|
||||
);
|
||||
|
||||
if (existingResource.length > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"Resource with that protocol and port already exists"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
if (proxyPort === 443 || proxyPort === 80) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Port 80 and 443 are reserved for https resources"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// make sure the full domain is unique
|
||||
const existingResource = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.fullDomain, fullDomain));
|
||||
|
||||
if (existingResource.length > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"Resource with that domain already exists"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const newResource = await trx
|
||||
.insert(resources)
|
||||
.values({
|
||||
siteId,
|
||||
fullDomain,
|
||||
fullDomain: http? fullDomain : null,
|
||||
orgId,
|
||||
name,
|
||||
subdomain,
|
||||
http,
|
||||
protocol,
|
||||
proxyPort,
|
||||
ssl: true
|
||||
})
|
||||
.returning();
|
||||
|
@ -135,18 +209,6 @@ export async function createResource(
|
|||
});
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof SqliteError &&
|
||||
error.code === "SQLITE_CONSTRAINT_UNIQUE"
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"Resource with that subdomain already exists"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
|
|
|
@ -103,7 +103,7 @@ export async function deleteResource(
|
|||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
|
||||
removeTargets(newt.newtId, targetsToBeRemoved);
|
||||
removeTargets(newt.newtId, targetsToBeRemoved, deletedResource.protocol);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,7 +63,10 @@ function queryResources(
|
|||
passwordId: resourcePassword.passwordId,
|
||||
pincodeId: resourcePincode.pincodeId,
|
||||
sso: resources.sso,
|
||||
whitelist: resources.emailWhitelistEnabled
|
||||
whitelist: resources.emailWhitelistEnabled,
|
||||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
|
@ -93,7 +96,10 @@ function queryResources(
|
|||
passwordId: resourcePassword.passwordId,
|
||||
sso: resources.sso,
|
||||
pincodeId: resourcePincode.pincodeId,
|
||||
whitelist: resources.emailWhitelistEnabled
|
||||
whitelist: resources.emailWhitelistEnabled,
|
||||
http: resources.http,
|
||||
protocol: resources.protocol,
|
||||
proxyPort: resources.proxyPort
|
||||
})
|
||||
.from(resources)
|
||||
.leftJoin(sites, eq(resources.siteId, sites.siteId))
|
||||
|
|
|
@ -26,8 +26,8 @@ const updateResourceBodySchema = z
|
|||
ssl: z.boolean().optional(),
|
||||
sso: z.boolean().optional(),
|
||||
blockAccess: z.boolean().optional(),
|
||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||
emailWhitelistEnabled: z.boolean().optional()
|
||||
// siteId: z.number(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
|
|
|
@ -53,9 +53,8 @@ const createTargetParamsSchema = z
|
|||
const createTargetSchema = z
|
||||
.object({
|
||||
ip: domainSchema,
|
||||
method: z.string().min(1).max(10),
|
||||
method: z.string().optional().nullable(),
|
||||
port: z.number().int().min(1).max(65535),
|
||||
protocol: z.string().optional(),
|
||||
enabled: z.boolean().default(true)
|
||||
})
|
||||
.strict();
|
||||
|
@ -94,9 +93,7 @@ export async function createTarget(
|
|||
|
||||
// get the resource
|
||||
const [resource] = await db
|
||||
.select({
|
||||
siteId: resources.siteId
|
||||
})
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId));
|
||||
|
||||
|
@ -130,7 +127,6 @@ export async function createTarget(
|
|||
.insert(targets)
|
||||
.values({
|
||||
resourceId,
|
||||
protocol: "tcp", // hard code for now
|
||||
...targetData
|
||||
})
|
||||
.returning();
|
||||
|
@ -163,7 +159,6 @@ export async function createTarget(
|
|||
.insert(targets)
|
||||
.values({
|
||||
resourceId,
|
||||
protocol: "tcp", // hard code for now
|
||||
internalPort,
|
||||
...targetData
|
||||
})
|
||||
|
@ -186,7 +181,7 @@ export async function createTarget(
|
|||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
|
||||
addTargets(newt.newtId, newTarget);
|
||||
addTargets(newt.newtId, newTarget, resource.protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,9 +50,7 @@ export async function deleteTarget(
|
|||
}
|
||||
// get the resource
|
||||
const [resource] = await db
|
||||
.select({
|
||||
siteId: resources.siteId
|
||||
})
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, deletedTarget.resourceId!));
|
||||
|
||||
|
@ -110,7 +108,7 @@ export async function deleteTarget(
|
|||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
|
||||
removeTargets(newt.newtId, [deletedTarget]);
|
||||
removeTargets(newt.newtId, [deletedTarget], resource.protocol);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@ function queryTargets(resourceId: number) {
|
|||
ip: targets.ip,
|
||||
method: targets.method,
|
||||
port: targets.port,
|
||||
protocol: targets.protocol,
|
||||
enabled: targets.enabled,
|
||||
resourceId: targets.resourceId
|
||||
// resourceName: resources.name,
|
||||
|
|
|
@ -49,7 +49,7 @@ const updateTargetParamsSchema = z
|
|||
const updateTargetBodySchema = z
|
||||
.object({
|
||||
ip: domainSchema.optional(),
|
||||
method: z.string().min(1).max(10).optional(),
|
||||
method: z.string().min(1).max(10).optional().nullable(),
|
||||
port: z.number().int().min(1).max(65535).optional(),
|
||||
enabled: z.boolean().optional()
|
||||
})
|
||||
|
@ -103,9 +103,7 @@ export async function updateTarget(
|
|||
|
||||
// get the resource
|
||||
const [resource] = await db
|
||||
.select({
|
||||
siteId: resources.siteId
|
||||
})
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, target.resourceId!));
|
||||
|
||||
|
@ -167,7 +165,7 @@ export async function updateTarget(
|
|||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
|
||||
addTargets(newt.newtId, [updatedTarget]);
|
||||
addTargets(newt.newtId, [updatedTarget], resource.protocol);
|
||||
}
|
||||
}
|
||||
return response(res, {
|
||||
|
|
|
@ -1,173 +1,267 @@
|
|||
import { Request, Response } from "express";
|
||||
import db from "@server/db";
|
||||
import * as schema from "@server/db/schema";
|
||||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import config from "@server/lib/config";
|
||||
import { orgs, resources, sites, Target, targets } from "@server/db/schema";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
export async function traefikConfigProvider(
|
||||
_: Request,
|
||||
res: Response,
|
||||
res: Response
|
||||
): Promise<any> {
|
||||
try {
|
||||
const all = await db
|
||||
.select()
|
||||
.from(schema.targets)
|
||||
.innerJoin(
|
||||
schema.resources,
|
||||
eq(schema.targets.resourceId, schema.resources.resourceId),
|
||||
)
|
||||
.innerJoin(
|
||||
schema.orgs,
|
||||
eq(schema.resources.orgId, schema.orgs.orgId),
|
||||
)
|
||||
.innerJoin(
|
||||
schema.sites,
|
||||
eq(schema.sites.siteId, schema.resources.siteId),
|
||||
)
|
||||
.where(
|
||||
const allResources = await db
|
||||
.select({
|
||||
// Resource fields
|
||||
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,
|
||||
// Site fields
|
||||
site: {
|
||||
siteId: sites.siteId,
|
||||
type: sites.type,
|
||||
subnet: sites.subnet
|
||||
},
|
||||
// Org fields
|
||||
org: {
|
||||
orgId: orgs.orgId,
|
||||
domain: orgs.domain
|
||||
},
|
||||
// Targets as a subquery
|
||||
targets: sql<string>`json_group_array(json_object(
|
||||
'targetId', ${targets.targetId},
|
||||
'ip', ${targets.ip},
|
||||
'method', ${targets.method},
|
||||
'port', ${targets.port},
|
||||
'internalPort', ${targets.internalPort},
|
||||
'enabled', ${targets.enabled}
|
||||
))`.as("targets")
|
||||
})
|
||||
.from(resources)
|
||||
.innerJoin(sites, eq(sites.siteId, resources.siteId))
|
||||
.innerJoin(orgs, eq(resources.orgId, orgs.orgId))
|
||||
.leftJoin(
|
||||
targets,
|
||||
and(
|
||||
eq(schema.targets.enabled, true),
|
||||
isNotNull(schema.resources.subdomain),
|
||||
isNotNull(schema.orgs.domain),
|
||||
),
|
||||
);
|
||||
eq(targets.resourceId, resources.resourceId),
|
||||
eq(targets.enabled, true)
|
||||
)
|
||||
)
|
||||
.groupBy(resources.resourceId);
|
||||
|
||||
if (!all.length) {
|
||||
if (!allResources.length) {
|
||||
return res.status(HttpCode.OK).json({});
|
||||
}
|
||||
|
||||
const badgerMiddlewareName = "badger";
|
||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||
|
||||
const http: any = {
|
||||
routers: {},
|
||||
services: {},
|
||||
middlewares: {
|
||||
[badgerMiddlewareName]: {
|
||||
plugin: {
|
||||
[badgerMiddlewareName]: {
|
||||
apiBaseUrl: new URL(
|
||||
"/api/v1",
|
||||
`http://${config.getRawConfig().server.internal_hostname}:${config.getRawConfig().server.internal_port}`,
|
||||
).href,
|
||||
userSessionCookieName:
|
||||
config.getRawConfig().server.session_cookie_name,
|
||||
accessTokenQueryParam: config.getRawConfig().server.resource_access_token_param,
|
||||
resourceSessionRequestParam: config.getRawConfig().server.resource_session_request_param
|
||||
},
|
||||
},
|
||||
},
|
||||
[redirectHttpsMiddlewareName]: {
|
||||
redirectScheme: {
|
||||
scheme: "https"
|
||||
const config_output: any = {
|
||||
http: {
|
||||
middlewares: {
|
||||
[badgerMiddlewareName]: {
|
||||
plugin: {
|
||||
[badgerMiddlewareName]: {
|
||||
apiBaseUrl: new URL(
|
||||
"/api/v1",
|
||||
`http://${config.getRawConfig().server.internal_hostname}:${
|
||||
config.getRawConfig().server
|
||||
.internal_port
|
||||
}`
|
||||
).href,
|
||||
userSessionCookieName:
|
||||
config.getRawConfig().server
|
||||
.session_cookie_name,
|
||||
accessTokenQueryParam:
|
||||
config.getRawConfig().server
|
||||
.resource_access_token_param,
|
||||
resourceSessionRequestParam:
|
||||
config.getRawConfig().server
|
||||
.resource_session_request_param
|
||||
}
|
||||
}
|
||||
},
|
||||
[redirectHttpsMiddlewareName]: {
|
||||
redirectScheme: {
|
||||
scheme: "https"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
for (const item of all) {
|
||||
const target = item.targets;
|
||||
const resource = item.resources;
|
||||
const site = item.sites;
|
||||
const org = item.orgs;
|
||||
|
||||
const routerName = `${target.targetId}-router`;
|
||||
const serviceName = `${target.targetId}-service`;
|
||||
for (const resource of allResources) {
|
||||
const targets = JSON.parse(resource.targets);
|
||||
const site = resource.site;
|
||||
const org = resource.org;
|
||||
|
||||
if (!resource || !resource.subdomain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!org || !org.domain) {
|
||||
if (!org.domain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const routerName = `${resource.resourceId}-router`;
|
||||
const serviceName = `${resource.resourceId}-service`;
|
||||
const fullDomain = `${resource.subdomain}.${org.domain}`;
|
||||
|
||||
const domainParts = fullDomain.split(".");
|
||||
let wildCard;
|
||||
if (domainParts.length <= 2) {
|
||||
wildCard = `*.${domainParts.join(".")}`;
|
||||
} else {
|
||||
wildCard = `*.${domainParts.slice(1).join(".")}`;
|
||||
}
|
||||
if (resource.http) {
|
||||
// HTTP configuration remains the same
|
||||
if (!resource.subdomain) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tls = {
|
||||
certResolver: config.getRawConfig().traefik.cert_resolver,
|
||||
...(config.getRawConfig().traefik.prefer_wildcard_cert
|
||||
? {
|
||||
domains: [
|
||||
{
|
||||
main: wildCard,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
if (
|
||||
targets.filter(
|
||||
(target: Target) => target.internalPort != null
|
||||
).length == 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || [];
|
||||
// add routers and services empty objects if they don't exist
|
||||
if (!config_output.http.routers) {
|
||||
config_output.http.routers = {};
|
||||
}
|
||||
|
||||
http.routers![routerName] = {
|
||||
entryPoints: [
|
||||
resource.ssl
|
||||
? config.getRawConfig().traefik.https_entrypoint
|
||||
: config.getRawConfig().traefik.http_entrypoint,
|
||||
],
|
||||
middlewares: [badgerMiddlewareName, ...additionalMiddlewares],
|
||||
service: serviceName,
|
||||
rule: `Host(\`${fullDomain}\`)`,
|
||||
...(resource.ssl ? { tls } : {}),
|
||||
};
|
||||
if (!config_output.http.services) {
|
||||
config_output.http.services = {};
|
||||
}
|
||||
|
||||
if (resource.ssl) {
|
||||
http.routers![routerName + "-redirect"] = {
|
||||
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
|
||||
middlewares: [redirectHttpsMiddlewareName],
|
||||
const domainParts = fullDomain.split(".");
|
||||
let wildCard;
|
||||
if (domainParts.length <= 2) {
|
||||
wildCard = `*.${domainParts.join(".")}`;
|
||||
} else {
|
||||
wildCard = `*.${domainParts.slice(1).join(".")}`;
|
||||
}
|
||||
|
||||
const tls = {
|
||||
certResolver: config.getRawConfig().traefik.cert_resolver,
|
||||
...(config.getRawConfig().traefik.prefer_wildcard_cert
|
||||
? {
|
||||
domains: [
|
||||
{
|
||||
main: wildCard
|
||||
}
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
|
||||
config_output.http.routers![routerName] = {
|
||||
entryPoints: [
|
||||
resource.ssl
|
||||
? config.getRawConfig().traefik.https_entrypoint
|
||||
: config.getRawConfig().traefik.http_entrypoint
|
||||
],
|
||||
middlewares: [badgerMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: `Host(\`${fullDomain}\`)`,
|
||||
...(resource.ssl ? { tls } : {})
|
||||
};
|
||||
}
|
||||
|
||||
if (site.type === "newt") {
|
||||
const ip = site.subnet.split("/")[0];
|
||||
http.services![serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `${target.method}://${ip}:${target.internalPort}`,
|
||||
},
|
||||
if (resource.ssl) {
|
||||
config_output.http.routers![routerName + "-redirect"] = {
|
||||
entryPoints: [
|
||||
config.getRawConfig().traefik.http_entrypoint
|
||||
],
|
||||
},
|
||||
middlewares: [redirectHttpsMiddlewareName],
|
||||
service: serviceName,
|
||||
rule: `Host(\`${fullDomain}\`)`
|
||||
};
|
||||
}
|
||||
|
||||
config_output.http.services![serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: targets
|
||||
.filter(
|
||||
(target: Target) => target.internalPort != null
|
||||
)
|
||||
.map((target: Target) => {
|
||||
if (
|
||||
site.type === "local" ||
|
||||
site.type === "wireguard"
|
||||
) {
|
||||
return {
|
||||
url: `${target.method}://${target.ip}:${target.port}`
|
||||
};
|
||||
} else if (site.type === "newt") {
|
||||
const ip = site.subnet.split("/")[0];
|
||||
return {
|
||||
url: `${target.method}://${ip}:${target.internalPort}`
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
} else if (site.type === "wireguard") {
|
||||
http.services![serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `${target.method}://${target.ip}:${target.port}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
} else {
|
||||
// Non-HTTP (TCP/UDP) configuration
|
||||
const protocol = resource.protocol.toLowerCase();
|
||||
const port = resource.proxyPort;
|
||||
|
||||
if (!port) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
targets.filter(
|
||||
(target: Target) => target.internalPort != null
|
||||
).length == 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!config_output[protocol]) {
|
||||
config_output[protocol] = {
|
||||
routers: {},
|
||||
services: {}
|
||||
};
|
||||
}
|
||||
|
||||
config_output[protocol].routers[routerName] = {
|
||||
entryPoints: [`${protocol}-${port}`],
|
||||
service: serviceName,
|
||||
...(protocol === "tcp" ? { rule: "HostSNI(`*`)" } : {})
|
||||
};
|
||||
} else if (site.type === "local") {
|
||||
http.services![serviceName] = {
|
||||
|
||||
config_output[protocol].services[serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: [
|
||||
{
|
||||
url: `${target.method}://${target.ip}:${target.port}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
servers: targets
|
||||
.filter(
|
||||
(target: Target) => target.internalPort != null
|
||||
)
|
||||
.map((target: Target) => {
|
||||
if (
|
||||
site.type === "local" ||
|
||||
site.type === "wireguard"
|
||||
) {
|
||||
return {
|
||||
address: `${target.ip}:${target.port}`
|
||||
};
|
||||
} else if (site.type === "newt") {
|
||||
const ip = site.subnet.split("/")[0];
|
||||
return {
|
||||
address: `${ip}:${target.internalPort}`
|
||||
};
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(HttpCode.OK).json({ http });
|
||||
return res.status(HttpCode.OK).json(config_output);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to build traefik config: ${e}`);
|
||||
return res.status(HttpCode.INTERNAL_SERVER_ERROR).json({
|
||||
error: "Failed to build traefik config",
|
||||
error: "Failed to build traefik config"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@ import {
|
|||
emailVerificationCodes,
|
||||
passwordResetTokens,
|
||||
resourceOtp,
|
||||
resources,
|
||||
resourceWhitelist,
|
||||
targets,
|
||||
userInvites,
|
||||
users
|
||||
} from "@server/db/schema";
|
||||
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
|
@ -33,11 +35,81 @@ export default async function migration() {
|
|||
});
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"We were unable to make all emails lower case in the database."
|
||||
"We were unable to make all emails lower case in the database. You can safely ignore this error."
|
||||
);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
await db.transaction(async (trx) => {
|
||||
|
||||
const resourcesAll = await trx.select({
|
||||
resourceId: resources.resourceId,
|
||||
fullDomain: resources.fullDomain,
|
||||
subdomain: resources.subdomain
|
||||
}).from(resources);
|
||||
|
||||
trx.run(`DROP INDEX resources_fullDomain_unique;`)
|
||||
trx.run(`ALTER TABLE resources
|
||||
DROP COLUMN fullDomain;
|
||||
`)
|
||||
trx.run(`ALTER TABLE resources
|
||||
DROP COLUMN subdomain;
|
||||
`)
|
||||
trx.run(sql`ALTER TABLE resources
|
||||
ADD COLUMN fullDomain TEXT;
|
||||
`)
|
||||
trx.run(sql`ALTER TABLE resources
|
||||
ADD COLUMN subdomain TEXT;
|
||||
`)
|
||||
trx.run(sql`ALTER TABLE resources
|
||||
ADD COLUMN http INTEGER DEFAULT true NOT NULL;
|
||||
`)
|
||||
trx.run(sql`ALTER TABLE resources
|
||||
ADD COLUMN protocol TEXT DEFAULT 'tcp' NOT NULL;
|
||||
`)
|
||||
trx.run(sql`ALTER TABLE resources
|
||||
ADD COLUMN proxyPort INTEGER;
|
||||
`)
|
||||
|
||||
// write the new fullDomain and subdomain values back to the database
|
||||
for (const resource of resourcesAll) {
|
||||
await trx.update(resources).set({
|
||||
fullDomain: resource.fullDomain,
|
||||
subdomain: resource.subdomain
|
||||
}).where(eq(resources.resourceId, resource.resourceId));
|
||||
}
|
||||
|
||||
const targetsAll = await trx.select({
|
||||
targetId: targets.targetId,
|
||||
method: targets.method
|
||||
}).from(targets);
|
||||
|
||||
trx.run(`ALTER TABLE targets
|
||||
DROP COLUMN method;
|
||||
`)
|
||||
trx.run(`ALTER TABLE targets
|
||||
DROP COLUMN protocol;
|
||||
`)
|
||||
trx.run(sql`ALTER TABLE targets
|
||||
ADD COLUMN method TEXT;
|
||||
`)
|
||||
|
||||
// write the new method and protocol values back to the database
|
||||
for (const target of targetsAll) {
|
||||
await trx.update(targets).set({
|
||||
method: target.method
|
||||
}).where(eq(targets.targetId, target.targetId));
|
||||
}
|
||||
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"We were unable to make the changes to the targets and resources tables."
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
// Determine which config file exists
|
||||
const filePaths = [configFilePath1, configFilePath2];
|
||||
|
@ -81,7 +153,24 @@ export default async function migration() {
|
|||
"traefik_config.yml"
|
||||
);
|
||||
|
||||
// Define schema for traefik config validation
|
||||
const schema = z.object({
|
||||
entryPoints: z
|
||||
.object({
|
||||
websecure: z
|
||||
.object({
|
||||
address: z.string(),
|
||||
transport: z
|
||||
.object({
|
||||
respondingTimeouts: z.object({
|
||||
readTimeout: z.string()
|
||||
})
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.optional(),
|
||||
experimental: z.object({
|
||||
plugins: z.object({
|
||||
badger: z.object({
|
||||
|
@ -101,26 +190,39 @@ export default async function migration() {
|
|||
throw new Error(fromZodError(parsedConfig.error).toString());
|
||||
}
|
||||
|
||||
// Ensure websecure entrypoint exists
|
||||
if (traefikConfig.entryPoints?.websecure) {
|
||||
// Add transport configuration
|
||||
traefikConfig.entryPoints.websecure.transport = {
|
||||
respondingTimeouts: {
|
||||
readTimeout: "30m"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
traefikConfig.experimental.plugins.badger.version = "v1.0.0-beta.3";
|
||||
|
||||
const updatedTraefikYaml = yaml.dump(traefikConfig);
|
||||
|
||||
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
|
||||
|
||||
console.log(
|
||||
"Updated the version of Badger in your Traefik configuration to v1.0.0-beta.3."
|
||||
"Updated the version of Badger in your Traefik configuration to v1.0.0-beta.3 and added readTimeout to websecure entrypoint in your Traefik configuration.."
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"We were unable to update the version of Badger in your Traefik configuration. Please update it manually."
|
||||
"We were unable to update the version of Badger in your Traefik configuration. Please update it manually to at least v1.0.0-beta.3. https://github.com/fosrl/badger"
|
||||
);
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
await db.transaction(async (trx) => {
|
||||
trx.run(sql`ALTER TABLE 'resourceSessions' ADD 'isRequestToken' integer;`);
|
||||
trx.run(sql`ALTER TABLE 'resourceSessions' ADD 'userSessionId' text REFERENCES session(id);`);
|
||||
trx.run(
|
||||
sql`ALTER TABLE 'resourceSessions' ADD 'isRequestToken' integer;`
|
||||
);
|
||||
trx.run(
|
||||
sql`ALTER TABLE 'resourceSessions' ADD 'userSessionId' text REFERENCES session(id);`
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(
|
||||
|
|
|
@ -45,21 +45,56 @@ import {
|
|||
} from "@app/components/ui/command";
|
||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||
import CustomDomainInput from "./[resourceId]/CustomDomainInput";
|
||||
import { Axios, AxiosResponse } from "axios";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { Resource } from "@server/db/schema";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { Switch } from "@app/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from "@app/components/ui/select";
|
||||
|
||||
const accountFormSchema = z.object({
|
||||
subdomain: subdomainSchema,
|
||||
name: z.string(),
|
||||
siteId: z.number()
|
||||
});
|
||||
const createResourceFormSchema = z
|
||||
.object({
|
||||
subdomain: z
|
||||
.union([
|
||||
z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
|
||||
"Invalid subdomain format"
|
||||
)
|
||||
.min(1, "Subdomain must be at least 1 character long")
|
||||
.transform((val) => val.toLowerCase()),
|
||||
z.string().optional()
|
||||
])
|
||||
.optional(),
|
||||
name: z.string().min(1).max(255),
|
||||
siteId: z.number(),
|
||||
http: z.boolean(),
|
||||
protocol: z.string(),
|
||||
proxyPort: z.number().int().min(1).max(65535).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
if (data.http === true) {
|
||||
return true;
|
||||
}
|
||||
return !!data.proxyPort;
|
||||
},
|
||||
{
|
||||
message: "Port number is required for non-HTTP resources",
|
||||
path: ["proxyPort"]
|
||||
}
|
||||
);
|
||||
|
||||
type AccountFormValues = z.infer<typeof accountFormSchema>;
|
||||
type CreateResourceFormValues = z.infer<typeof createResourceFormSchema>;
|
||||
|
||||
type CreateResourceFormProps = {
|
||||
open: boolean;
|
||||
|
@ -81,15 +116,18 @@ export default function CreateResourceForm({
|
|||
const router = useRouter();
|
||||
|
||||
const { org } = useOrgContext();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
||||
const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
|
||||
|
||||
const form = useForm<AccountFormValues>({
|
||||
resolver: zodResolver(accountFormSchema),
|
||||
const form = useForm<CreateResourceFormValues>({
|
||||
resolver: zodResolver(createResourceFormSchema),
|
||||
defaultValues: {
|
||||
subdomain: "",
|
||||
name: "My Resource"
|
||||
name: "My Resource",
|
||||
http: true,
|
||||
protocol: "tcp"
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -112,7 +150,7 @@ export default function CreateResourceForm({
|
|||
fetchSites();
|
||||
}, [open]);
|
||||
|
||||
async function onSubmit(data: AccountFormValues) {
|
||||
async function onSubmit(data: CreateResourceFormValues) {
|
||||
console.log(data);
|
||||
|
||||
const res = await api
|
||||
|
@ -120,8 +158,10 @@ export default function CreateResourceForm({
|
|||
`/org/${orgId}/site/${data.siteId}/resource/`,
|
||||
{
|
||||
name: data.name,
|
||||
subdomain: data.subdomain
|
||||
// subdomain: data.subdomain,
|
||||
subdomain: data.http ? data.subdomain : undefined,
|
||||
http: data.http,
|
||||
protocol: data.protocol,
|
||||
proxyPort: data.http ? undefined : data.proxyPort
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
|
@ -188,34 +228,151 @@ export default function CreateResourceForm({
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subdomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Subdomain</FormLabel>
|
||||
<FormControl>
|
||||
<CustomDomainInput
|
||||
value={field.value}
|
||||
domainSuffix={domainSuffix}
|
||||
placeholder="Enter subdomain"
|
||||
onChange={(value) =>
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the fully qualified
|
||||
domain name that will be used to
|
||||
access the resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!env.flags.allowRawResources || (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="http"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
HTTP Resource
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Toggle if this is an
|
||||
HTTP resource or a raw
|
||||
TCP/UDP resource
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{form.watch("http") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subdomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Subdomain</FormLabel>
|
||||
<FormControl>
|
||||
<CustomDomainInput
|
||||
value={
|
||||
field.value ?? ""
|
||||
}
|
||||
domainSuffix={
|
||||
domainSuffix
|
||||
}
|
||||
placeholder="Enter subdomain"
|
||||
onChange={(value) =>
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the fully qualified
|
||||
domain name that will be
|
||||
used to access the resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!form.watch("http") && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Protocol
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a protocol" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">
|
||||
TCP
|
||||
</SelectItem>
|
||||
<SelectItem value="udp">
|
||||
UDP
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
The protocol to use for
|
||||
the resource
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Port Number
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter port number"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: null
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
The port number to proxy
|
||||
requests to (required
|
||||
for non-HTTP resources)
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="siteId"
|
||||
|
|
|
@ -39,6 +39,9 @@ export type ResourceRow = {
|
|||
site: string;
|
||||
siteId: string;
|
||||
hasAuth: boolean;
|
||||
http: boolean;
|
||||
protocol: string;
|
||||
proxyPort: number | null;
|
||||
};
|
||||
|
||||
type ResourcesTableProps = {
|
||||
|
@ -158,12 +161,28 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
|||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "domain",
|
||||
header: "Full URL",
|
||||
accessorKey: "protocol",
|
||||
header: "Protocol",
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
<span>{resourceRow.protocol.toUpperCase()}</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "domain",
|
||||
header: "Access",
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
<div>
|
||||
{!resourceRow.http ? (
|
||||
<CopyToClipboard text={resourceRow.proxyPort!.toString()} isLink={false} />
|
||||
) : (
|
||||
<CopyToClipboard text={resourceRow.domain} isLink={true} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -186,17 +205,23 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
|||
const resourceRow = row.original;
|
||||
return (
|
||||
<div>
|
||||
{resourceRow.hasAuth ? (
|
||||
<span className="text-green-500 flex items-center space-x-2">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
<span>Protected</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-yellow-500 flex items-center space-x-2">
|
||||
<ShieldOff className="w-4 h-4" />
|
||||
<span>Not Protected</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
||||
{!resourceRow.http ? (
|
||||
<span>--</span>
|
||||
) :
|
||||
resourceRow.hasAuth ? (
|
||||
<span className="text-green-500 flex items-center space-x-2">
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
<span>Protected</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-yellow-500 flex items-center space-x-2">
|
||||
<ShieldOff className="w-4 h-4" />
|
||||
<span>Not Protected</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,12 +2,8 @@
|
|||
|
||||
import { useState } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
InfoIcon,
|
||||
LinkIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
ShieldCheck,
|
||||
ShieldOff
|
||||
} from "lucide-react";
|
||||
|
@ -42,37 +38,65 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
|||
</AlertTitle>
|
||||
<AlertDescription className="mt-4">
|
||||
<InfoSections>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Authentication</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{authInfo.password ||
|
||||
authInfo.pincode ||
|
||||
authInfo.sso ||
|
||||
authInfo.whitelist ? (
|
||||
<div className="flex items-start space-x-2 text-green-500">
|
||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||
<span>
|
||||
This resource is protected with at least
|
||||
one auth method.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2 text-yellow-500">
|
||||
<ShieldOff className="w-4 h-4" />
|
||||
<span>
|
||||
Anyone can access this resource.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<Separator orientation="vertical" />
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>URL</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard text={fullUrl} isLink={true} />
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
{resource.http ? (
|
||||
<>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
Authentication
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{authInfo.password ||
|
||||
authInfo.pincode ||
|
||||
authInfo.sso ||
|
||||
authInfo.whitelist ? (
|
||||
<div className="flex items-start space-x-2 text-green-500">
|
||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||
<span>
|
||||
This resource is protected with
|
||||
at least one auth method.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2 text-yellow-500">
|
||||
<ShieldOff className="w-4 h-4" />
|
||||
<span>
|
||||
Anyone can access this resource.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<Separator orientation="vertical" />
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>URL</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
text={fullUrl}
|
||||
isLink={true}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Protocol</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span>{resource.protocol.toUpperCase()}</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<Separator orientation="vertical" />
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Port</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
text={resource.proxyPort!.toString()}
|
||||
isLink={false}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</>
|
||||
)}
|
||||
</InfoSections>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
|
|
@ -94,7 +94,7 @@ const domainSchema = z
|
|||
|
||||
const addTargetSchema = z.object({
|
||||
ip: domainSchema,
|
||||
method: z.string(),
|
||||
method: z.string().nullable(),
|
||||
port: z.coerce.number().int().positive()
|
||||
// protocol: z.string(),
|
||||
});
|
||||
|
@ -129,9 +129,9 @@ export default function ReverseProxyTargets(props: {
|
|||
const addTargetForm = useForm({
|
||||
resolver: zodResolver(addTargetSchema),
|
||||
defaultValues: {
|
||||
ip: "",
|
||||
method: "http",
|
||||
port: 80
|
||||
ip: "localhost",
|
||||
method: resource.http ? "http" : null,
|
||||
port: resource.http ? 80 : resource.proxyPort || 1234
|
||||
// protocol: "TCP",
|
||||
}
|
||||
});
|
||||
|
@ -330,26 +330,6 @@ export default function ReverseProxyTargets(props: {
|
|||
}
|
||||
|
||||
const columns: ColumnDef<LocalTarget>[] = [
|
||||
{
|
||||
accessorKey: "method",
|
||||
header: "Method",
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.method}
|
||||
onValueChange={(value) =>
|
||||
updateTarget(row.original.targetId, { method: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="min-w-[100px]">
|
||||
{row.original.method}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">http</SelectItem>
|
||||
<SelectItem value="https">https</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "ip",
|
||||
header: "IP / Hostname",
|
||||
|
@ -436,6 +416,32 @@ export default function ReverseProxyTargets(props: {
|
|||
}
|
||||
];
|
||||
|
||||
if (resource.http) {
|
||||
const methodCol: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "method",
|
||||
header: "Method",
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.method ?? ""}
|
||||
onValueChange={(value) =>
|
||||
updateTarget(row.original.targetId, { method: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="min-w-[100px]">
|
||||
{row.original.method}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">http</SelectItem>
|
||||
<SelectItem value="https">https</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
};
|
||||
|
||||
// add this to the first column
|
||||
columns.unshift(methodCol);
|
||||
}
|
||||
|
||||
const table = useReactTable({
|
||||
data: targets,
|
||||
columns,
|
||||
|
@ -451,7 +457,7 @@ export default function ReverseProxyTargets(props: {
|
|||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
{/* SSL Section */}
|
||||
{resource.http && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
|
@ -473,7 +479,7 @@ export default function ReverseProxyTargets(props: {
|
|||
/>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
)}
|
||||
{/* Targets Section */}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
|
@ -491,6 +497,8 @@ export default function ReverseProxyTargets(props: {
|
|||
className="space-y-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{resource.http && (
|
||||
|
||||
<FormField
|
||||
control={addTargetForm.control}
|
||||
name="method"
|
||||
|
@ -499,7 +507,7 @@ export default function ReverseProxyTargets(props: {
|
|||
<FormLabel>Method</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value || undefined}
|
||||
onValueChange={(value) => {
|
||||
addTargetForm.setValue(
|
||||
"method",
|
||||
|
@ -524,6 +532,8 @@ export default function ReverseProxyTargets(props: {
|
|||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={addTargetForm.control}
|
||||
name="ip"
|
||||
|
@ -637,6 +647,9 @@ export default function ReverseProxyTargets(props: {
|
|||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<SettingsSectionDescription>
|
||||
Multiple targets will get load balanced by Traefik. You can use this for high availability.
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
|
|
|
@ -13,22 +13,7 @@ import {
|
|||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from "@/components/ui/command";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from "@/components/ui/popover";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { ListSitesResponse } from "@server/routers/site";
|
||||
import { useEffect, useState } from "react";
|
||||
|
@ -49,15 +34,25 @@ import {
|
|||
} from "@app/components/Settings";
|
||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||
import CustomDomainInput from "../CustomDomainInput";
|
||||
import ResourceInfoBox from "../ResourceInfoBox";
|
||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string(),
|
||||
subdomain: subdomainSchema
|
||||
// siteId: z.number(),
|
||||
subdomain: z
|
||||
.union([
|
||||
z
|
||||
.string()
|
||||
.regex(
|
||||
/^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9-_]+$/,
|
||||
"Invalid subdomain format"
|
||||
)
|
||||
.min(1, "Subdomain must be at least 1 character long")
|
||||
.transform((val) => val.toLowerCase()),
|
||||
z.string().optional()
|
||||
])
|
||||
.optional(),
|
||||
name: z.string().min(1).max(255),
|
||||
proxyPort: z.number().int().min(1).max(65535).optional()
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
|
@ -81,8 +76,8 @@ export default function GeneralForm() {
|
|||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: resource.name,
|
||||
subdomain: resource.subdomain
|
||||
// siteId: resource.siteId!,
|
||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||
proxyPort: resource.proxyPort ? resource.proxyPort : undefined
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
|
@ -169,33 +164,78 @@ export default function GeneralForm() {
|
|||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subdomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Subdomain</FormLabel>
|
||||
<FormControl>
|
||||
<CustomDomainInput
|
||||
value={field.value}
|
||||
domainSuffix={domainSuffix}
|
||||
placeholder="Enter subdomain"
|
||||
onChange={(value) =>
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the subdomain that will
|
||||
be used to access the resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{resource.http ? (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="subdomain"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Subdomain</FormLabel>
|
||||
<FormControl>
|
||||
<CustomDomainInput
|
||||
value={
|
||||
field.value || ""
|
||||
}
|
||||
domainSuffix={
|
||||
domainSuffix
|
||||
}
|
||||
placeholder="Enter subdomain"
|
||||
onChange={(value) =>
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
value
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the subdomain that
|
||||
will be used to access the
|
||||
resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
Port Number
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter port number"
|
||||
value={
|
||||
field.value ?? ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: null
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the port that will
|
||||
be used to access the
|
||||
resource.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
|
|
|
@ -90,13 +90,16 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
|||
title: "Connectivity",
|
||||
href: `/{orgId}/settings/resources/{resourceId}/connectivity`
|
||||
// icon: <Cloud className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
}
|
||||
];
|
||||
|
||||
if (resource.http) {
|
||||
sidebarNavItems.push({
|
||||
title: "Authentication",
|
||||
href: `/{orgId}/settings/resources/{resourceId}/authentication`
|
||||
// icon: <Shield className="w-4 h-4" />,
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -53,6 +53,9 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
|||
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
|
||||
site: resource.siteName || "None",
|
||||
siteId: resource.siteId || "Unknown",
|
||||
protocol: resource.protocol,
|
||||
proxyPort: resource.proxyPort,
|
||||
http: resource.http,
|
||||
hasAuth:
|
||||
resource.sso ||
|
||||
resource.pincodeId !== null ||
|
||||
|
|
|
@ -26,7 +26,9 @@ export function pullEnv(): Env {
|
|||
emailVerificationRequired:
|
||||
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true"
|
||||
? true
|
||||
: false
|
||||
: false,
|
||||
allowRawResources:
|
||||
process.env.FLAGS_ALLOW_RAW_RESOURCES === "true" ? true : false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -17,5 +17,6 @@ export type Env = {
|
|||
disableSignupWithoutInvite: boolean;
|
||||
disableUserCreateOrg: boolean;
|
||||
emailVerificationRequired: boolean;
|
||||
allowRawResources: boolean;
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue