Merge branch 'main' into dev

This commit is contained in:
Owen 2025-08-10 10:16:47 -07:00
commit 55b4a9eddb
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
43 changed files with 898 additions and 346 deletions

View file

@ -18,6 +18,7 @@ import (
"syscall"
"text/template"
"time"
"net"
"golang.org/x/term"
)
@ -76,6 +77,17 @@ func main() {
fmt.Println("Lets get started!")
fmt.Println("")
for _, p := range []int{80, 443} {
if err := checkPortsAvailable(p); err != nil {
fmt.Fprintln(os.Stderr, err)
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly")
os.Exit(1)
}
}
reader := bufio.NewReader(os.Stdin)
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
@ -778,3 +790,21 @@ func run(name string, args ...string) error {
cmd.Stderr = os.Stderr
return cmd.Run()
}
func checkPortsAvailable(port int) error {
addr := fmt.Sprintf(":%d", port)
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf(
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
port, err,
)
}
if closeErr := ln.Close(); closeErr != nil {
fmt.Fprintf(os.Stderr,
"WARNING: failed to close test listener on port %d: %v\n",
port, closeErr,
)
}
return nil
}

1073
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -50,15 +50,15 @@
"@radix-ui/react-tabs": "1.1.12",
"@radix-ui/react-toast": "1.2.14",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-email/components": "0.3.1",
"@react-email/render": "^1.1.2",
"@react-email/components": "0.5.0",
"@react-email/render": "^1.2.0",
"@simplewebauthn/browser": "^13.1.0",
"@simplewebauthn/server": "^9.0.3",
"@react-email/tailwind": "1.2.1",
"@react-email/tailwind": "1.2.2",
"@tailwindcss/forms": "^0.5.10",
"@tanstack/react-table": "8.21.3",
"arctic": "^3.7.0",
"axios": "1.10.0",
"axios": "1.11.0",
"better-sqlite3": "11.7.0",
"canvas-confetti": "1.9.3",
"class-variance-authority": "^0.7.1",
@ -69,9 +69,9 @@
"cookies": "^0.9.1",
"cors": "2.8.5",
"crypto-js": "^4.2.0",
"drizzle-orm": "0.44.2",
"eslint": "9.31.0",
"eslint-config-next": "15.3.5",
"drizzle-orm": "0.44.4",
"eslint": "9.32.0",
"eslint-config-next": "15.4.6",
"express": "4.21.2",
"express-rate-limit": "7.5.1",
"glob": "11.0.3",
@ -82,28 +82,28 @@
"jmespath": "^0.16.0",
"js-yaml": "4.1.0",
"jsonwebtoken": "^9.0.2",
"lucide-react": "0.525.0",
"lucide-react": "0.536.0",
"moment": "2.30.1",
"next": "15.3.5",
"next": "15.4.6",
"next-intl": "^4.3.4",
"next-themes": "0.4.6",
"node-cache": "5.1.2",
"node-fetch": "3.3.2",
"nodemailer": "7.0.5",
"npm": "^11.4.2",
"npm": "^11.5.2",
"oslo": "1.2.1",
"pg": "^8.16.2",
"qrcode.react": "4.2.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-easy-sort": "^1.6.0",
"react-hook-form": "7.60.0",
"react-hook-form": "7.62.0",
"react-icons": "^5.5.0",
"rebuild": "0.1.2",
"semver": "^7.7.2",
"swagger-ui-express": "^5.0.1",
"tailwind-merge": "3.3.1",
"tw-animate-css": "^1.3.5",
"tw-animate-css": "^1.3.6",
"uuid": "^11.1.0",
"vaul": "1.1.2",
"winston": "3.17.0",
@ -114,7 +114,7 @@
"yargs": "18.0.0"
},
"devDependencies": {
"@dotenvx/dotenvx": "1.47.6",
"@dotenvx/dotenvx": "1.48.4",
"@esbuild-plugins/tsconfig-paths": "0.1.2",
"@tailwindcss/postcss": "^4.1.10",
"@types/better-sqlite3": "7.6.12",
@ -129,8 +129,8 @@
"@types/node": "^24",
"@types/nodemailer": "6.4.17",
"@types/pg": "8.15.4",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/react": "19.1.9",
"@types/react-dom": "19.1.7",
"@types/semver": "^7.7.0",
"@types/swagger-ui-express": "^4.1.8",
"@types/ws": "8.18.1",
@ -139,12 +139,12 @@
"esbuild": "0.25.6",
"esbuild-node-externals": "1.18.0",
"postcss": "^8",
"react-email": "4.1.0",
"react-email": "4.2.8",
"tailwindcss": "^4.1.4",
"tsc-alias": "1.8.16",
"tsx": "4.20.3",
"typescript": "^5",
"typescript-eslint": "^8.36.0"
"typescript-eslint": "^8.39.0"
},
"overrides": {
"emblor": {

View file

@ -271,7 +271,7 @@ export async function getNextAvailableClientSubnet(
)
].filter((address) => address !== null) as string[];
let subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
@ -289,7 +289,7 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
const addresses = existingAddresses.map((org) => org.subnet!);
let subnet = findNextAvailableCidr(
const subnet = findNextAvailableCidr(
addresses,
config.getRawConfig().orgs.block_size,
config.getRawConfig().orgs.subnet_group

View file

@ -1,6 +1,6 @@
import { MemoryStore, Store } from "express-rate-limit";
export function createStore(): Store {
let rateLimitStore: Store = new MemoryStore();
const rateLimitStore: Store = new MemoryStore();
return rateLimitStore;
}

View file

@ -222,7 +222,7 @@ export async function listAccessTokens(
(resource) => resource.resourceId
);
let countQuery: any = db
const countQuery: any = db
.select({ count: count() })
.from(resources)
.where(inArray(resources.resourceId, accessibleResourceIds));

View file

@ -48,7 +48,7 @@ export async function getAllRelays(
}
// Fetch exit node
let [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey));
const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey));
if (!exitNode) {
return next(createHttpError(HttpCode.NOT_FOUND, "Exit node not found"));
}
@ -63,7 +63,7 @@ export async function getAllRelays(
}
// Initialize mappings object for multi-peer support
let mappings: { [key: string]: ProxyMapping } = {};
const mappings: { [key: string]: ProxyMapping } = {};
// Process each site
for (const site of sitesRes) {

View file

@ -112,7 +112,7 @@ export async function getConfig(
)
);
let peers = await Promise.all(
const peers = await Promise.all(
sitesRes.map(async (site) => {
if (site.type === "wireguard") {
return {

View file

@ -68,7 +68,7 @@ export async function createOidcIdp(
);
}
let {
const {
clientId,
clientSecret,
authUrl,

View file

@ -85,7 +85,7 @@ export async function updateOidcIdp(
}
const { idpId } = parsedParams.data;
let {
const {
clientId,
clientSecret,
authUrl,

View file

@ -238,7 +238,7 @@ export async function validateOidcCallback(
const defaultRoleMapping = existingIdp.idp.defaultRoleMapping;
const defaultOrgMapping = existingIdp.idp.defaultOrgMapping;
let userOrgInfo: { orgId: string; roleId: number }[] = [];
const userOrgInfo: { orgId: string; roleId: number }[] = [];
for (const org of allOrgs) {
const [idpOrgRes] = await db
.select()
@ -314,7 +314,7 @@ export async function validateOidcCallback(
let existingUserId = existingUser?.userId;
let orgUserCounts: { orgId: string; userCount: number }[] = [];
const orgUserCounts: { orgId: string; userCount: number }[] = [];
// sync the user with the orgs and roles
await db.transaction(async (trx) => {

View file

@ -55,7 +55,7 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
);
if (currentConnections.count >= maxConnections) {
return null
return null;
}
weight =

View file

@ -37,7 +37,7 @@ export const startOfflineChecker = (): void => {
}, OFFLINE_CHECK_INTERVAL);
logger.info("Started offline checker interval");
}
};
/**
* Stops the background interval that checks for offline clients
@ -48,7 +48,7 @@ export const stopOfflineChecker = (): void => {
offlineCheckerInterval = null;
logger.info("Stopped offline checker interval");
}
}
};
/**
* Handles ping messages from clients and responds with pong

View file

@ -102,7 +102,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
.where(eq(clientSites.clientId, client.clientId));
// Prepare an array to store site configurations
let siteConfigurations = [];
const siteConfigurations = [];
logger.debug(`Found ${sitesData.length} sites for client ${client.clientId}`);
if (sitesData.length === 0) {

View file

@ -35,7 +35,7 @@ const listResourceRulesSchema = z.object({
});
function queryResourceRules(resourceId: number) {
let baseQuery = db
const baseQuery = db
.select({
ruleId: resourceRules.ruleId,
resourceId: resourceRules.resourceId,
@ -117,7 +117,7 @@ export async function listResourceRules(
const baseQuery = queryResourceRules(resourceId);
let countQuery = db
const countQuery = db
.select({ count: sql<number>`cast(count(*) as integer)` })
.from(resourceRules)
.where(eq(resourceRules.resourceId, resourceId));

View file

@ -231,7 +231,7 @@ export async function listResources(
(resource) => resource.resourceId
);
let countQuery: any = db
const countQuery: any = db
.select({ count: count() })
.from(resources)
.where(inArray(resources.resourceId, accessibleResourceIds));

View file

@ -100,7 +100,7 @@ export async function listRoles(
const { orgId } = parsedParams.data;
let countQuery: any = db
const countQuery: any = db
.select({ count: sql<number>`cast(count(*) as integer)` })
.from(roles)
.where(eq(roles.orgId, orgId));

View file

@ -176,7 +176,7 @@ export async function listSites(
const accessibleSiteIds = accessibleSites.map((site) => site.siteId);
const baseQuery = querySites(orgId, accessibleSiteIds);
let countQuery = db
const countQuery = db
.select({ count: count() })
.from(sites)
.where(

View file

@ -86,7 +86,7 @@ export async function pickSiteDefaults(
.where(eq(sites.exitNodeId, exitNode.exitNodeId));
// TODO: we need to lock this subnet for some time so someone else does not take it
let subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null);
const subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null);
// exclude the exit node address by replacing after the / with a site block size
subnets.push(
exitNode.address.replace(

View file

@ -2,7 +2,7 @@ import { db } from "@server/db";
import { resources, targets } from "@server/db";
import { eq } from "drizzle-orm";
let currentBannedPorts: number[] = [];
const currentBannedPorts: number[] = [];
export async function pickPort(siteId: number): Promise<{
internalPort: number;
@ -15,8 +15,8 @@ export async function pickPort(siteId: number): Promise<{
// TODO: is this all inefficient?
// Fetch targets for all resources of this site
let targetIps: string[] = [];
let targetInternalPorts: number[] = [];
const targetIps: string[] = [];
const targetInternalPorts: number[] = [];
await Promise.all(
resourcesRes.map(async (resource) => {
const targetsRes = await db

View file

@ -35,7 +35,7 @@ const listTargetsSchema = z.object({
});
function queryTargets(resourceId: number) {
let baseQuery = db
const baseQuery = db
.select({
targetId: targets.targetId,
ip: targets.ip,
@ -99,7 +99,7 @@ export async function listTargets(
const baseQuery = queryTargets(resourceId);
let countQuery = db
const countQuery = db
.select({ count: sql<number>`cast(count(*) as integer)` })
.from(targets)
.where(eq(targets.resourceId, resourceId));

View file

@ -62,7 +62,7 @@ const wss: WebSocketServer = new WebSocketServer({ noServer: true });
const NODE_ID = uuidv4();
// Client tracking map (local to this node)
let connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
const connectedClients: Map<string, AuthenticatedWebSocket[]> = new Map();
// Helper to get map key
const getClientMapKey = (clientId: string) => clientId;

View file

@ -36,8 +36,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
if (rawConfig.server?.trust_proxy) {

View file

@ -23,8 +23,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
delete rawConfig.server.secure_cookies;

View file

@ -25,8 +25,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
if (!rawConfig.flags) {

View file

@ -30,8 +30,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
const baseDomain = rawConfig.app.base_domain;

View file

@ -22,8 +22,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
// Validate the structure

View file

@ -22,8 +22,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
// Validate the structure

View file

@ -25,8 +25,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
// Validate the structure

View file

@ -23,8 +23,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
// Validate the structure

View file

@ -58,8 +58,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
rawConfig.server.resource_session_request_param =
@ -122,7 +122,7 @@ export default async function migration() {
const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
const traefikConfig = yaml.load(traefikFileContents) as any;
let parsedConfig: any = schema.safeParse(traefikConfig);
const parsedConfig: any = schema.safeParse(traefikConfig);
if (parsedConfig.success) {
// Ensure websecure entrypoint exists
@ -179,7 +179,7 @@ export default async function migration() {
const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
const traefikConfig = yaml.load(traefikFileContents) as any;
let parsedConfig: any = schema.safeParse(traefikConfig);
const parsedConfig: any = schema.safeParse(traefikConfig);
if (parsedConfig.success) {
// delete permanent from redirect-to-https middleware

View file

@ -43,8 +43,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
if (!rawConfig.flags) {

View file

@ -177,7 +177,8 @@ export default async function migration() {
}
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any = yaml.load(fileContents);
let rawConfig: any;
rawConfig = yaml.load(fileContents);
if (!rawConfig.server.secret) {
rawConfig.server.secret = generateIdFromEntropySize(32);

View file

@ -44,8 +44,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
if (rawConfig.cors?.headers) {

View file

@ -45,8 +45,8 @@ export default async function migration() {
}
// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
let rawConfig: any;
rawConfig = yaml.load(fileContents);
if (rawConfig.server?.trust_proxy) {

View file

@ -108,7 +108,7 @@ export default function Page() {
async function onSubmit(data: CreateFormValues) {
setCreateLoading(true);
let payload: CreateOrgApiKeyBody = {
const payload: CreateOrgApiKeyBody = {
name: data.name
};

View file

@ -290,7 +290,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
columns={columns}
data={rows}
addClient={() => {
router.push(`/${orgId}/settings/clients/create`)
router.push(`/${orgId}/settings/clients/create`);
}}
/>
</>

View file

@ -280,7 +280,7 @@ export default function Page() {
return;
}
let payload: CreateClientBody = {
const payload: CreateClientBody = {
name: data.name,
type: data.method as "olm",
siteIds: data.siteIds.map((site) => parseInt(site.id)),

View file

@ -29,7 +29,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { isEnabled, isAvailable } = useDockerSocket(site!);
const t = useTranslations();
let fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
const fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
return (
<Alert>

View file

@ -327,7 +327,7 @@ export default function ReverseProxyTargets(props: {
setProxySettingsLoading(true);
// Save targets
for (let target of targets) {
for (const target of targets) {
const data = {
ip: target.ip,
port: target.port,

View file

@ -271,7 +271,7 @@ export default function ResourceRules(props: {
}
// Save rules
for (let rule of rules) {
for (const rule of rules) {
const data = {
action: rule.action,
match: rule.match,
@ -348,7 +348,7 @@ export default function ResourceRules(props: {
setRules([
...rules.map((r) => {
let res = {
const res = {
...r,
new: false,
updated: false

View file

@ -106,7 +106,7 @@ export default function Page() {
async function onSubmit(data: CreateFormValues) {
setCreateLoading(true);
let payload: CreateOrgApiKeyBody = {
const payload: CreateOrgApiKeyBody = {
name: data.name
};

View file

@ -34,7 +34,7 @@ export default async function RootLayout({
const env = pullEnv();
const locale = await getLocale();
let supporterData = {
const supporterData = {
visible: true
} as any;