mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-22 12:56:37 +00:00
Compare commits
23 Commits
1.11.0-s.4
...
1.11.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3af1e0ef56 | ||
|
|
08b7d6735c | ||
|
|
a91ebd1e91 | ||
|
|
312e03b4eb | ||
|
|
e8a57e432c | ||
|
|
bca2eef2e8 | ||
|
|
ec7211a15d | ||
|
|
46807c6477 | ||
|
|
b578786e62 | ||
|
|
2e0ad8d262 | ||
|
|
003f0cfa6d | ||
|
|
ee3df081ef | ||
|
|
08eeb12519 | ||
|
|
e66c6b2505 | ||
|
|
d2a880d9c8 | ||
|
|
edc0b86470 | ||
|
|
aebe6b80b7 | ||
|
|
4d87333b43 | ||
|
|
ef32f3ed5a | ||
|
|
216ded3034 | ||
|
|
cb59fe2cee | ||
|
|
7776f6d09c | ||
|
|
ba96332313 |
10
Makefile
10
Makefile
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
||||||
build-release-arm:
|
build-release:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest \
|
||||||
@@ -17,7 +17,7 @@ build-release-arm:
|
|||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push .
|
--push .
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
@@ -26,7 +26,7 @@ build-release-arm:
|
|||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-latest \
|
--tag fosrl/pangolin:ee-latest \
|
||||||
@@ -35,7 +35,7 @@ build-release-arm:
|
|||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -51,7 +51,8 @@ Check out the [quick install guide](https://docs.digpangolin.com/self-host/quick
|
|||||||
|
|
||||||
| <img width=500 /> | Description |
|
| <img width=500 /> | Description |
|
||||||
|-----------------|--------------|
|
|-----------------|--------------|
|
||||||
| **Self-Host** | Free, open source, and AGPL-3 compliant. |
|
| **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. |
|
||||||
|
| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
|
||||||
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.digpangolin.com/manage/remote-node/nodes) and connect to our control plane. |
|
| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.digpangolin.com/manage/remote-node/nodes) and connect to our control plane. |
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
@@ -60,10 +61,10 @@ Pangolin packages everything you need for seamless application access and exposu
|
|||||||
|
|
||||||
| <img width=500 /> | <img width=500 /> |
|
| <img width=500 /> | <img width=500 /> |
|
||||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
||||||
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" /><tr></tr> |
|
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
|
||||||
| **Reverse proxy across networks anywhere**<br /><br />Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | <img src="public/screenshots/sites.png" /><tr></tr> |
|
| **Reverse proxy across networks anywhere**<br /><br />Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||||
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" /><tr></tr> |
|
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
|
||||||
| **Quickly connect Pangolin sites**<br /><br />Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | <img src="public/clip.gif" /><tr></tr> |
|
| **Quickly connect Pangolin sites**<br /><br />Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ services:
|
|||||||
pangolin:
|
pangolin:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3004
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
- pangolin-data-certificates:/var/certificates
|
|
||||||
- pangolin-data-dynamic:/var/dynamic
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||||
interval: "10s"
|
interval: "10s"
|
||||||
@@ -22,7 +20,7 @@ services:
|
|||||||
pangolin:
|
pangolin:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3004
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||||
volumes:
|
volumes:
|
||||||
@@ -56,16 +54,9 @@ services:
|
|||||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||||
# Shared volume for certificates and dynamic config in file mode
|
|
||||||
- pangolin-data-certificates:/var/certificates:ro
|
|
||||||
- pangolin-data-dynamic:/var/dynamic:ro
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||||
|
|
||||||
volumes:
|
|
||||||
pangolin-data-dynamic:
|
|
||||||
pangolin-data-certificates:
|
|
||||||
@@ -1893,5 +1893,6 @@
|
|||||||
"pathRewriteExact": "Exact",
|
"pathRewriteExact": "Exact",
|
||||||
"pathRewriteRegex": "Regex",
|
"pathRewriteRegex": "Regex",
|
||||||
"pathRewriteStrip": "Strip",
|
"pathRewriteStrip": "Strip",
|
||||||
"pathRewriteStripLabel": "strip"
|
"pathRewriteStripLabel": "strip",
|
||||||
|
"sidebarEnableEnterpriseLicense": "Enable Enterprise License"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,16 @@ import { setHostMeta } from "@server/lib/hostMeta";
|
|||||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
||||||
import { initCleanup } from "#dynamic/cleanup";
|
import { initCleanup } from "#dynamic/cleanup";
|
||||||
|
import license from "#dynamic/license/license";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await setHostMeta();
|
await setHostMeta();
|
||||||
|
|
||||||
await config.initServer();
|
await config.initServer();
|
||||||
|
|
||||||
|
license.setServerSecret(config.getRawConfig().server.secret!);
|
||||||
|
await license.check();
|
||||||
|
|
||||||
await runSetupFunctions();
|
await runSetupFunctions();
|
||||||
|
|
||||||
initTelemetryClient();
|
initTelemetryClient();
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
|
|||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { SupporterKey, supporterKey } from "@server/db";
|
import { SupporterKey, supporterKey } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { license } from "#dynamic/license/license";
|
|
||||||
import { configSchema, readConfigFile } from "./readConfigFile";
|
import { configSchema, readConfigFile } from "./readConfigFile";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export class Config {
|
export class Config {
|
||||||
private rawConfig!: z.infer<typeof configSchema>;
|
private rawConfig!: z.infer<typeof configSchema>;
|
||||||
@@ -103,16 +101,10 @@ export class Config {
|
|||||||
throw new Error("Config not loaded. Call load() first.");
|
throw new Error("Config not loaded. Call load() first.");
|
||||||
}
|
}
|
||||||
|
|
||||||
license.setServerSecret(this.rawConfig.server.secret!);
|
|
||||||
|
|
||||||
await this.checkKeyStatus();
|
await this.checkKeyStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkKeyStatus() {
|
private async checkKeyStatus() {
|
||||||
if (build === "enterprise") {
|
|
||||||
await license.check();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (build == "oss") {
|
if (build == "oss") {
|
||||||
this.checkSupporterKey();
|
this.checkSupporterKey();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { db, targetHealthCheck } from "@server/db";
|
import { db, targetHealthCheck } from "@server/db";
|
||||||
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
import {
|
||||||
|
and,
|
||||||
|
eq,
|
||||||
|
inArray,
|
||||||
|
or,
|
||||||
|
isNull,
|
||||||
|
ne,
|
||||||
|
isNotNull,
|
||||||
|
desc,
|
||||||
|
sql
|
||||||
|
} from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { resources, sites, Target, targets } from "@server/db";
|
import { resources, sites, Target, targets } from "@server/db";
|
||||||
@@ -78,7 +88,13 @@ export async function getTraefikConfig(
|
|||||||
and(
|
and(
|
||||||
eq(targets.enabled, true),
|
eq(targets.enabled, true),
|
||||||
eq(resources.enabled, true),
|
eq(resources.enabled, true),
|
||||||
or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)),
|
or(
|
||||||
|
eq(sites.exitNodeId, exitNodeId),
|
||||||
|
and(
|
||||||
|
isNull(sites.exitNodeId),
|
||||||
|
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)` // only allow local sites if "local" is in siteTypes
|
||||||
|
)
|
||||||
|
),
|
||||||
or(
|
or(
|
||||||
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
||||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||||
|
|||||||
@@ -19,18 +19,26 @@ import * as fs from "fs";
|
|||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
const encryptionKeyPath =
|
let encryptionKeyPath = "";
|
||||||
config.getRawPrivateConfig().server.encryption_key_path;
|
let encryptionKeyHex = "";
|
||||||
|
let encryptionKey: Buffer;
|
||||||
|
function loadEncryptData() {
|
||||||
|
if (encryptionKey) {
|
||||||
|
return; // already loaded
|
||||||
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(encryptionKeyPath)) {
|
encryptionKeyPath = config.getRawPrivateConfig().server.encryption_key_path;
|
||||||
throw new Error(
|
|
||||||
"Encryption key file not found. Please generate one first."
|
if (!fs.existsSync(encryptionKeyPath)) {
|
||||||
);
|
throw new Error(
|
||||||
|
"Encryption key file not found. Please generate one first."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim();
|
||||||
|
encryptionKey = Buffer.from(encryptionKeyHex, "hex");
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim();
|
|
||||||
const encryptionKey = Buffer.from(encryptionKeyHex, "hex");
|
|
||||||
|
|
||||||
// Define the return type for clarity and type safety
|
// Define the return type for clarity and type safety
|
||||||
export type CertificateResult = {
|
export type CertificateResult = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -50,6 +58,9 @@ export async function getValidCertificatesForDomains(
|
|||||||
domains: Set<string>,
|
domains: Set<string>,
|
||||||
useCache: boolean = true
|
useCache: boolean = true
|
||||||
): Promise<Array<CertificateResult>> {
|
): Promise<Array<CertificateResult>> {
|
||||||
|
|
||||||
|
loadEncryptData(); // Ensure encryption key is loaded
|
||||||
|
|
||||||
const finalResults: CertificateResult[] = [];
|
const finalResults: CertificateResult[] = [];
|
||||||
const domainsToQuery = new Set<string>();
|
const domainsToQuery = new Set<string>();
|
||||||
|
|
||||||
@@ -151,7 +162,9 @@ export async function getValidCertificatesForDomains(
|
|||||||
|
|
||||||
// If a certificate was found, format it, add to results, and cache it
|
// If a certificate was found, format it, add to results, and cache it
|
||||||
if (foundCert) {
|
if (foundCert) {
|
||||||
logger.debug(`Creating result cert for ${domain} using cert from ${foundCert.domain}`);
|
logger.debug(
|
||||||
|
`Creating result cert for ${domain} using cert from ${foundCert.domain}`
|
||||||
|
);
|
||||||
const resultCert: CertificateResult = {
|
const resultCert: CertificateResult = {
|
||||||
id: foundCert.certId,
|
id: foundCert.certId,
|
||||||
domain: foundCert.domain, // The actual domain of the cert record
|
domain: foundCert.domain, // The actual domain of the cert record
|
||||||
@@ -172,7 +185,6 @@ export async function getValidCertificatesForDomains(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const decryptedResults = decryptFinalResults(finalResults);
|
const decryptedResults = decryptFinalResults(finalResults);
|
||||||
return decryptedResults;
|
return decryptedResults;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
privateConfigSchema,
|
privateConfigSchema,
|
||||||
readPrivateConfigFile
|
readPrivateConfigFile
|
||||||
} from "#private/lib/readConfigFile";
|
} from "#private/lib/readConfigFile";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
export class PrivateConfig {
|
export class PrivateConfig {
|
||||||
private rawPrivateConfig!: z.infer<typeof privateConfigSchema>;
|
private rawPrivateConfig!: z.infer<typeof privateConfigSchema>;
|
||||||
@@ -44,115 +43,104 @@ export class PrivateConfig {
|
|||||||
throw new Error(`Invalid private configuration file: ${errors}`);
|
throw new Error(`Invalid private configuration file: ${errors}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedPrivateConfig.branding?.colors) {
|
this.rawPrivateConfig = parsedPrivateConfig;
|
||||||
|
|
||||||
|
if (this.rawPrivateConfig.branding?.colors) {
|
||||||
process.env.BRANDING_COLORS = JSON.stringify(
|
process.env.BRANDING_COLORS = JSON.stringify(
|
||||||
parsedPrivateConfig.branding?.colors
|
this.rawPrivateConfig.branding?.colors
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedPrivateConfig.branding?.logo?.light_path) {
|
if (this.rawPrivateConfig.branding?.logo?.light_path) {
|
||||||
process.env.BRANDING_LOGO_LIGHT_PATH =
|
process.env.BRANDING_LOGO_LIGHT_PATH =
|
||||||
parsedPrivateConfig.branding?.logo?.light_path;
|
this.rawPrivateConfig.branding?.logo?.light_path;
|
||||||
}
|
}
|
||||||
if (parsedPrivateConfig.branding?.logo?.dark_path) {
|
if (this.rawPrivateConfig.branding?.logo?.dark_path) {
|
||||||
process.env.BRANDING_LOGO_DARK_PATH =
|
process.env.BRANDING_LOGO_DARK_PATH =
|
||||||
parsedPrivateConfig.branding?.logo?.dark_path || undefined;
|
this.rawPrivateConfig.branding?.logo?.dark_path || undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (build != "oss") {
|
process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding
|
||||||
if (parsedPrivateConfig.branding?.logo?.light_path) {
|
?.logo?.auth_page?.width
|
||||||
process.env.BRANDING_LOGO_LIGHT_PATH =
|
? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString()
|
||||||
parsedPrivateConfig.branding?.logo?.light_path;
|
: undefined;
|
||||||
}
|
process.env.BRANDING_LOGO_AUTH_HEIGHT = this.rawPrivateConfig.branding
|
||||||
if (parsedPrivateConfig.branding?.logo?.dark_path) {
|
?.logo?.auth_page?.height
|
||||||
process.env.BRANDING_LOGO_DARK_PATH =
|
? this.rawPrivateConfig.branding?.logo?.auth_page?.height.toString()
|
||||||
parsedPrivateConfig.branding?.logo?.dark_path || undefined;
|
: undefined;
|
||||||
}
|
|
||||||
|
|
||||||
process.env.BRANDING_LOGO_AUTH_WIDTH = parsedPrivateConfig.branding
|
process.env.BRANDING_LOGO_NAVBAR_WIDTH = this.rawPrivateConfig.branding
|
||||||
?.logo?.auth_page?.width
|
?.logo?.navbar?.width
|
||||||
? parsedPrivateConfig.branding?.logo?.auth_page?.width.toString()
|
? this.rawPrivateConfig.branding?.logo?.navbar?.width.toString()
|
||||||
: undefined;
|
: undefined;
|
||||||
process.env.BRANDING_LOGO_AUTH_HEIGHT = parsedPrivateConfig.branding
|
process.env.BRANDING_LOGO_NAVBAR_HEIGHT = this.rawPrivateConfig.branding
|
||||||
?.logo?.auth_page?.height
|
?.logo?.navbar?.height
|
||||||
? parsedPrivateConfig.branding?.logo?.auth_page?.height.toString()
|
? this.rawPrivateConfig.branding?.logo?.navbar?.height.toString()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
process.env.BRANDING_LOGO_NAVBAR_WIDTH = parsedPrivateConfig
|
process.env.BRANDING_FAVICON_PATH =
|
||||||
.branding?.logo?.navbar?.width
|
this.rawPrivateConfig.branding?.favicon_path;
|
||||||
? parsedPrivateConfig.branding?.logo?.navbar?.width.toString()
|
|
||||||
: undefined;
|
|
||||||
process.env.BRANDING_LOGO_NAVBAR_HEIGHT = parsedPrivateConfig
|
|
||||||
.branding?.logo?.navbar?.height
|
|
||||||
? parsedPrivateConfig.branding?.logo?.navbar?.height.toString()
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
process.env.BRANDING_FAVICON_PATH =
|
process.env.BRANDING_APP_NAME =
|
||||||
parsedPrivateConfig.branding?.favicon_path;
|
this.rawPrivateConfig.branding?.app_name || "Pangolin";
|
||||||
|
|
||||||
process.env.BRANDING_APP_NAME =
|
if (this.rawPrivateConfig.branding?.footer) {
|
||||||
parsedPrivateConfig.branding?.app_name || "Pangolin";
|
process.env.BRANDING_FOOTER = JSON.stringify(
|
||||||
|
this.rawPrivateConfig.branding?.footer
|
||||||
if (parsedPrivateConfig.branding?.footer) {
|
);
|
||||||
process.env.BRANDING_FOOTER = JSON.stringify(
|
|
||||||
parsedPrivateConfig.branding?.footer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
process.env.LOGIN_PAGE_TITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.login_page?.title_text || "";
|
|
||||||
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.login_page?.subtitle_text || "";
|
|
||||||
|
|
||||||
process.env.SIGNUP_PAGE_TITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.signup_page?.title_text || "";
|
|
||||||
process.env.SIGNUP_PAGE_SUBTITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.signup_page?.subtitle_text || "";
|
|
||||||
|
|
||||||
process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY =
|
|
||||||
parsedPrivateConfig.branding?.resource_auth_page
|
|
||||||
?.hide_powered_by === true
|
|
||||||
? "true"
|
|
||||||
: "false";
|
|
||||||
process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO =
|
|
||||||
parsedPrivateConfig.branding?.resource_auth_page?.show_logo ===
|
|
||||||
true
|
|
||||||
? "true"
|
|
||||||
: "false";
|
|
||||||
process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.resource_auth_page?.title_text ||
|
|
||||||
"";
|
|
||||||
process.env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT =
|
|
||||||
parsedPrivateConfig.branding?.resource_auth_page
|
|
||||||
?.subtitle_text || "";
|
|
||||||
|
|
||||||
if (parsedPrivateConfig.branding?.background_image_path) {
|
|
||||||
process.env.BACKGROUND_IMAGE_PATH =
|
|
||||||
parsedPrivateConfig.branding?.background_image_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedPrivateConfig.server.reo_client_id) {
|
|
||||||
process.env.REO_CLIENT_ID =
|
|
||||||
parsedPrivateConfig.server.reo_client_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedPrivateConfig.stripe?.s3Bucket) {
|
|
||||||
process.env.S3_BUCKET = parsedPrivateConfig.stripe.s3Bucket;
|
|
||||||
}
|
|
||||||
if (parsedPrivateConfig.stripe?.localFilePath) {
|
|
||||||
process.env.LOCAL_FILE_PATH =
|
|
||||||
parsedPrivateConfig.stripe.localFilePath;
|
|
||||||
}
|
|
||||||
if (parsedPrivateConfig.stripe?.s3Region) {
|
|
||||||
process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
|
|
||||||
}
|
|
||||||
if (parsedPrivateConfig.flags.use_pangolin_dns) {
|
|
||||||
process.env.USE_PANGOLIN_DNS =
|
|
||||||
parsedPrivateConfig.flags.use_pangolin_dns.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rawPrivateConfig = parsedPrivateConfig;
|
process.env.LOGIN_PAGE_TITLE_TEXT =
|
||||||
|
this.rawPrivateConfig.branding?.login_page?.title_text || "";
|
||||||
|
process.env.LOGIN_PAGE_SUBTITLE_TEXT =
|
||||||
|
this.rawPrivateConfig.branding?.login_page?.subtitle_text || "";
|
||||||
|
|
||||||
|
process.env.SIGNUP_PAGE_TITLE_TEXT =
|
||||||
|
this.rawPrivateConfig.branding?.signup_page?.title_text || "";
|
||||||
|
process.env.SIGNUP_PAGE_SUBTITLE_TEXT =
|
||||||
|
this.rawPrivateConfig.branding?.signup_page?.subtitle_text || "";
|
||||||
|
|
||||||
|
process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY =
|
||||||
|
this.rawPrivateConfig.branding?.resource_auth_page
|
||||||
|
?.hide_powered_by === true
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO =
|
||||||
|
this.rawPrivateConfig.branding?.resource_auth_page?.show_logo ===
|
||||||
|
true
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT =
|
||||||
|
this.rawPrivateConfig.branding?.resource_auth_page?.title_text ||
|
||||||
|
"";
|
||||||
|
process.env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT =
|
||||||
|
this.rawPrivateConfig.branding?.resource_auth_page?.subtitle_text ||
|
||||||
|
"";
|
||||||
|
|
||||||
|
if (this.rawPrivateConfig.branding?.background_image_path) {
|
||||||
|
process.env.BACKGROUND_IMAGE_PATH =
|
||||||
|
this.rawPrivateConfig.branding?.background_image_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rawPrivateConfig.server.reo_client_id) {
|
||||||
|
process.env.REO_CLIENT_ID =
|
||||||
|
this.rawPrivateConfig.server.reo_client_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.rawPrivateConfig.stripe?.s3Bucket) {
|
||||||
|
process.env.S3_BUCKET = this.rawPrivateConfig.stripe.s3Bucket;
|
||||||
|
}
|
||||||
|
if (this.rawPrivateConfig.stripe?.localFilePath) {
|
||||||
|
process.env.LOCAL_FILE_PATH =
|
||||||
|
this.rawPrivateConfig.stripe.localFilePath;
|
||||||
|
}
|
||||||
|
if (this.rawPrivateConfig.stripe?.s3Region) {
|
||||||
|
process.env.S3_REGION = this.rawPrivateConfig.stripe.s3Region;
|
||||||
|
}
|
||||||
|
if (this.rawPrivateConfig.flags.use_pangolin_dns) {
|
||||||
|
process.env.USE_PANGOLIN_DNS =
|
||||||
|
this.rawPrivateConfig.flags.use_pangolin_dns.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRawPrivateConfig() {
|
public getRawPrivateConfig() {
|
||||||
|
|||||||
@@ -172,6 +172,15 @@ export function readPrivateConfigFile() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test if the config file is there
|
||||||
|
if (!fs.existsSync(privateConfigFilePath1)) {
|
||||||
|
// console.warn(
|
||||||
|
// `Private configuration file not found at ${privateConfigFilePath1}. Using default configuration.`
|
||||||
|
// );
|
||||||
|
// load the default values of the zod schema and return those
|
||||||
|
return privateConfigSchema.parse({});
|
||||||
|
}
|
||||||
|
|
||||||
const loadConfig = (configPath: string) => {
|
const loadConfig = (configPath: string) => {
|
||||||
try {
|
try {
|
||||||
const yamlContent = fs.readFileSync(configPath, "utf8");
|
const yamlContent = fs.readFileSync(configPath, "utf8");
|
||||||
|
|||||||
@@ -19,7 +19,17 @@ import {
|
|||||||
loginPage,
|
loginPage,
|
||||||
targetHealthCheck
|
targetHealthCheck
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
import {
|
||||||
|
and,
|
||||||
|
eq,
|
||||||
|
inArray,
|
||||||
|
or,
|
||||||
|
isNull,
|
||||||
|
ne,
|
||||||
|
isNotNull,
|
||||||
|
desc,
|
||||||
|
sql
|
||||||
|
} from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { orgs, resources, sites, Target, targets } from "@server/db";
|
import { orgs, resources, sites, Target, targets } from "@server/db";
|
||||||
@@ -110,15 +120,19 @@ export async function getTraefikConfig(
|
|||||||
and(
|
and(
|
||||||
eq(targets.enabled, true),
|
eq(targets.enabled, true),
|
||||||
eq(resources.enabled, true),
|
eq(resources.enabled, true),
|
||||||
// or(
|
or(
|
||||||
eq(sites.exitNodeId, exitNodeId),
|
eq(sites.exitNodeId, exitNodeId),
|
||||||
// isNull(sites.exitNodeId)
|
and(
|
||||||
// ),
|
isNull(sites.exitNodeId),
|
||||||
|
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)` // only allow local sites if "local" is in siteTypes
|
||||||
|
)
|
||||||
|
),
|
||||||
or(
|
or(
|
||||||
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
||||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||||
),
|
),
|
||||||
inArray(sites.type, siteTypes),
|
inArray(sites.type, siteTypes),
|
||||||
|
// lets rewrite this using sql
|
||||||
config.getRawConfig().traefik.allow_raw_resources
|
config.getRawConfig().traefik.allow_raw_resources
|
||||||
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
|
||||||
: eq(resources.http, true)
|
: eq(resources.http, true)
|
||||||
|
|||||||
@@ -61,7 +61,15 @@ export async function createExitNode(
|
|||||||
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
exitNode = exitNodeQuery;
|
// update the reachable at
|
||||||
|
[exitNode] = await db
|
||||||
|
.update(exitNodes)
|
||||||
|
.set({
|
||||||
|
reachableAt
|
||||||
|
})
|
||||||
|
.where(eq(exitNodes.exitNodeId, exitNodeQuery.exitNodeId))
|
||||||
|
.returning();
|
||||||
|
logger.info(`Updated exit node reachableAt to ${reachableAt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return exitNode;
|
return exitNode;
|
||||||
|
|||||||
@@ -292,11 +292,33 @@ hybridRouter.get(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let encryptionKeyPath = "";
|
||||||
|
let encryptionKeyHex = "";
|
||||||
|
let encryptionKey: Buffer;
|
||||||
|
function loadEncryptData() {
|
||||||
|
if (encryptionKey) {
|
||||||
|
return; // already loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionKeyPath = privateConfig.getRawPrivateConfig().server.encryption_key_path;
|
||||||
|
|
||||||
|
if (!fs.existsSync(encryptionKeyPath)) {
|
||||||
|
throw new Error(
|
||||||
|
"Encryption key file not found. Please generate one first."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim();
|
||||||
|
encryptionKey = Buffer.from(encryptionKeyHex, "hex");
|
||||||
|
}
|
||||||
|
|
||||||
// Get valid certificates for given domains (supports wildcard certs)
|
// Get valid certificates for given domains (supports wildcard certs)
|
||||||
hybridRouter.get(
|
hybridRouter.get(
|
||||||
"/certificates/domains",
|
"/certificates/domains",
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
|
loadEncryptData(); // Ensure encryption key is loaded
|
||||||
|
|
||||||
const parsed = getCertificatesByDomainsQuerySchema.safeParse(
|
const parsed = getCertificatesByDomainsQuerySchema.safeParse(
|
||||||
req.query
|
req.query
|
||||||
);
|
);
|
||||||
@@ -425,20 +447,6 @@ hybridRouter.get(
|
|||||||
filtered.push(cert);
|
filtered.push(cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptionKeyPath =
|
|
||||||
privateConfig.getRawPrivateConfig().server.encryption_key_path;
|
|
||||||
|
|
||||||
if (!fs.existsSync(encryptionKeyPath)) {
|
|
||||||
throw new Error(
|
|
||||||
"Encryption key file not found. Please generate one first."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptionKeyHex = fs
|
|
||||||
.readFileSync(encryptionKeyPath, "utf8")
|
|
||||||
.trim();
|
|
||||||
const encryptionKey = Buffer.from(encryptionKeyHex, "hex");
|
|
||||||
|
|
||||||
const result = filtered.map((cert) => {
|
const result = filtered.map((cert) => {
|
||||||
// Decrypt and save certificate file
|
// Decrypt and save certificate file
|
||||||
const decryptedCert = decryptData(
|
const decryptedCert = decryptData(
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export async function createExitNode(publicKey: string, reachableAt: string | un
|
|||||||
.where(eq(exitNodes.publicKey, publicKey))
|
.where(eq(exitNodes.publicKey, publicKey))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
logger.info(`Updated exit node`);
|
logger.info(`Updated exit node with reachableAt to ${reachableAt}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return exitNode;
|
return exitNode;
|
||||||
|
|||||||
@@ -117,27 +117,4 @@ export async function generateGerbilConfig(exitNode: ExitNode) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return configResponse;
|
return configResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNextAvailablePort(): Promise<number> {
|
|
||||||
// Get all existing ports from exitNodes table
|
|
||||||
const existingPorts = await db
|
|
||||||
.select({
|
|
||||||
listenPort: exitNodes.listenPort
|
|
||||||
})
|
|
||||||
.from(exitNodes);
|
|
||||||
|
|
||||||
// Find the first available port between 1024 and 65535
|
|
||||||
let nextPort = config.getRawConfig().gerbil.start_port;
|
|
||||||
for (const port of existingPorts) {
|
|
||||||
if (port.listenPort > nextPort) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
nextPort++;
|
|
||||||
if (nextPort > 65535) {
|
|
||||||
throw new Error("No available ports remaining in space");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextPort;
|
|
||||||
}
|
|
||||||
@@ -46,15 +46,24 @@ const createTargetSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
hcTimeout: z.number().int().positive().min(1).optional().nullable(),
|
hcTimeout: z.number().int().positive().min(1).optional().nullable(),
|
||||||
hcHeaders: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(),
|
hcHeaders: z
|
||||||
|
.array(z.object({ name: z.string(), value: z.string() }))
|
||||||
|
.nullable()
|
||||||
|
.optional(),
|
||||||
hcFollowRedirects: z.boolean().optional().nullable(),
|
hcFollowRedirects: z.boolean().optional().nullable(),
|
||||||
hcMethod: z.string().min(1).optional().nullable(),
|
hcMethod: z.string().min(1).optional().nullable(),
|
||||||
hcStatus: z.number().int().optional().nullable(),
|
hcStatus: z.number().int().optional().nullable(),
|
||||||
path: z.string().optional().nullable(),
|
path: z.string().optional().nullable(),
|
||||||
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
pathMatchType: z
|
||||||
|
.enum(["exact", "prefix", "regex"])
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
rewritePath: z.string().optional().nullable(),
|
rewritePath: z.string().optional().nullable(),
|
||||||
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
|
rewritePathType: z
|
||||||
priority: z.number().int().min(1).max(1000)
|
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
priority: z.number().int().min(1).max(1000).optional().nullable()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
@@ -164,12 +173,14 @@ export async function createTarget(
|
|||||||
|
|
||||||
let newTarget: Target[] = [];
|
let newTarget: Target[] = [];
|
||||||
let healthCheck: TargetHealthCheck[] = [];
|
let healthCheck: TargetHealthCheck[] = [];
|
||||||
|
let targetIps: string[] = [];
|
||||||
if (site.type == "local") {
|
if (site.type == "local") {
|
||||||
newTarget = await db
|
newTarget = await db
|
||||||
.insert(targets)
|
.insert(targets)
|
||||||
.values({
|
.values({
|
||||||
resourceId,
|
resourceId,
|
||||||
...targetData
|
...targetData,
|
||||||
|
priority: targetData.priority || 100
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
} else {
|
} else {
|
||||||
@@ -186,7 +197,7 @@ export async function createTarget(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { internalPort, targetIps } = await pickPort(
|
const { internalPort, targetIps: newTargetIps } = await pickPort(
|
||||||
site.siteId!,
|
site.siteId!,
|
||||||
db
|
db
|
||||||
);
|
);
|
||||||
@@ -214,61 +225,63 @@ export async function createTarget(
|
|||||||
pathMatchType: targetData.pathMatchType,
|
pathMatchType: targetData.pathMatchType,
|
||||||
rewritePath: targetData.rewritePath,
|
rewritePath: targetData.rewritePath,
|
||||||
rewritePathType: targetData.rewritePathType,
|
rewritePathType: targetData.rewritePathType,
|
||||||
priority: targetData.priority
|
priority: targetData.priority || 100
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
let hcHeaders = null;
|
|
||||||
if (targetData.hcHeaders) {
|
|
||||||
hcHeaders = JSON.stringify(targetData.hcHeaders);
|
|
||||||
}
|
|
||||||
|
|
||||||
healthCheck = await db
|
|
||||||
.insert(targetHealthCheck)
|
|
||||||
.values({
|
|
||||||
targetId: newTarget[0].targetId,
|
|
||||||
hcEnabled: targetData.hcEnabled ?? false,
|
|
||||||
hcPath: targetData.hcPath ?? null,
|
|
||||||
hcScheme: targetData.hcScheme ?? null,
|
|
||||||
hcMode: targetData.hcMode ?? null,
|
|
||||||
hcHostname: targetData.hcHostname ?? null,
|
|
||||||
hcPort: targetData.hcPort ?? null,
|
|
||||||
hcInterval: targetData.hcInterval ?? null,
|
|
||||||
hcUnhealthyInterval: targetData.hcUnhealthyInterval ?? null,
|
|
||||||
hcTimeout: targetData.hcTimeout ?? null,
|
|
||||||
hcHeaders: hcHeaders,
|
|
||||||
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
|
|
||||||
hcMethod: targetData.hcMethod ?? null,
|
|
||||||
hcStatus: targetData.hcStatus ?? null,
|
|
||||||
hcHealth: "unknown"
|
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// add the new target to the targetIps array
|
// add the new target to the targetIps array
|
||||||
targetIps.push(`${targetData.ip}/32`);
|
newTargetIps.push(`${targetData.ip}/32`);
|
||||||
|
|
||||||
if (site.pubKey) {
|
targetIps = newTargetIps;
|
||||||
if (site.type == "wireguard") {
|
}
|
||||||
await addPeer(site.exitNodeId!, {
|
|
||||||
publicKey: site.pubKey,
|
|
||||||
allowedIps: targetIps.flat()
|
|
||||||
});
|
|
||||||
} else if (site.type == "newt") {
|
|
||||||
// get the newt on the site by querying the newt table for siteId
|
|
||||||
const [newt] = await db
|
|
||||||
.select()
|
|
||||||
.from(newts)
|
|
||||||
.where(eq(newts.siteId, site.siteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
await addTargets(
|
let hcHeaders = null;
|
||||||
newt.newtId,
|
if (targetData.hcHeaders) {
|
||||||
newTarget,
|
hcHeaders = JSON.stringify(targetData.hcHeaders);
|
||||||
healthCheck,
|
}
|
||||||
resource.protocol,
|
|
||||||
resource.proxyPort
|
healthCheck = await db
|
||||||
);
|
.insert(targetHealthCheck)
|
||||||
}
|
.values({
|
||||||
|
targetId: newTarget[0].targetId,
|
||||||
|
hcEnabled: targetData.hcEnabled ?? false,
|
||||||
|
hcPath: targetData.hcPath ?? null,
|
||||||
|
hcScheme: targetData.hcScheme ?? null,
|
||||||
|
hcMode: targetData.hcMode ?? null,
|
||||||
|
hcHostname: targetData.hcHostname ?? null,
|
||||||
|
hcPort: targetData.hcPort ?? null,
|
||||||
|
hcInterval: targetData.hcInterval ?? null,
|
||||||
|
hcUnhealthyInterval: targetData.hcUnhealthyInterval ?? null,
|
||||||
|
hcTimeout: targetData.hcTimeout ?? null,
|
||||||
|
hcHeaders: hcHeaders,
|
||||||
|
hcFollowRedirects: targetData.hcFollowRedirects ?? null,
|
||||||
|
hcMethod: targetData.hcMethod ?? null,
|
||||||
|
hcStatus: targetData.hcStatus ?? null,
|
||||||
|
hcHealth: "unknown"
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (site.pubKey) {
|
||||||
|
if (site.type == "wireguard") {
|
||||||
|
await addPeer(site.exitNodeId!, {
|
||||||
|
publicKey: site.pubKey,
|
||||||
|
allowedIps: targetIps.flat()
|
||||||
|
});
|
||||||
|
} else if (site.type == "newt") {
|
||||||
|
// get the newt on the site by querying the newt table for siteId
|
||||||
|
const [newt] = await db
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, site.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
await addTargets(
|
||||||
|
newt.newtId,
|
||||||
|
newTarget,
|
||||||
|
healthCheck,
|
||||||
|
resource.protocol,
|
||||||
|
resource.proxyPort
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,6 +313,11 @@ export default async function migration() {
|
|||||||
dateCreated: string;
|
dateCreated: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
// Delete the old record
|
||||||
|
await db.execute(sql`
|
||||||
|
DELETE FROM "webauthnCredentials";
|
||||||
|
`);
|
||||||
|
|
||||||
for (const webauthnCredential of webauthnCredentials) {
|
for (const webauthnCredential of webauthnCredentials) {
|
||||||
const newCredentialId = isoBase64URL.fromBuffer(
|
const newCredentialId = isoBase64URL.fromBuffer(
|
||||||
new Uint8Array(
|
new Uint8Array(
|
||||||
@@ -325,12 +330,6 @@ export default async function migration() {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete the old record
|
|
||||||
await db.execute(sql`
|
|
||||||
DELETE FROM "webauthnCredentials"
|
|
||||||
WHERE "credentialId" = ${webauthnCredential.credentialId}
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Insert the updated record with converted values
|
// Insert the updated record with converted values
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated")
|
INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated")
|
||||||
|
|||||||
@@ -269,6 +269,8 @@ export default async function migration() {
|
|||||||
dateCreated: string;
|
dateCreated: string;
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
db.prepare(`DELETE FROM 'webauthnCredentials';`).run();
|
||||||
|
|
||||||
for (const webauthnCredential of webauthnCredentials) {
|
for (const webauthnCredential of webauthnCredentials) {
|
||||||
const newCredentialId = isoBase64URL.fromBuffer(
|
const newCredentialId = isoBase64URL.fromBuffer(
|
||||||
new Uint8Array(
|
new Uint8Array(
|
||||||
@@ -281,11 +283,6 @@ export default async function migration() {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Delete the old record
|
|
||||||
db.prepare(
|
|
||||||
`DELETE FROM 'webauthnCredentials' WHERE 'credentialId' = ?`
|
|
||||||
).run(webauthnCredential.credentialId);
|
|
||||||
|
|
||||||
// Insert the updated record with converted values
|
// Insert the updated record with converted values
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
`INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ const addTargetSchema = z
|
|||||||
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
priority: z.number().int().min(1).max(1000)
|
priority: z.number().int().min(1).max(1000).optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -205,6 +205,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
}) {
|
}) {
|
||||||
const params = use(props.params);
|
const params = use(props.params);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const { resource, updateResource } = useResourceContext();
|
const { resource, updateResource } = useResourceContext();
|
||||||
|
|
||||||
@@ -428,17 +429,19 @@ export default function ReverseProxyTargets(props: {
|
|||||||
}, [isAdvancedMode]);
|
}, [isAdvancedMode]);
|
||||||
|
|
||||||
function addNewTarget() {
|
function addNewTarget() {
|
||||||
|
const isHttp = resource.http;
|
||||||
|
|
||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
targetId: -Date.now(), // Use negative timestamp as temporary ID
|
targetId: -Date.now(), // Use negative timestamp as temporary ID
|
||||||
ip: "",
|
ip: "",
|
||||||
method: resource.http ? "http" : null,
|
method: isHttp ? "http" : null,
|
||||||
port: 0,
|
port: 0,
|
||||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
||||||
path: null,
|
path: isHttp ? null : null,
|
||||||
pathMatchType: null,
|
pathMatchType: isHttp ? null : null,
|
||||||
rewritePath: null,
|
rewritePath: isHttp ? null : null,
|
||||||
rewritePathType: null,
|
rewritePathType: isHttp ? null : null,
|
||||||
priority: 100,
|
priority: isHttp ? 100 : 100,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
hcEnabled: false,
|
hcEnabled: false,
|
||||||
@@ -514,25 +517,31 @@ export default function ReverseProxyTargets(props: {
|
|||||||
try {
|
try {
|
||||||
setTargetsLoading(true);
|
setTargetsLoading(true);
|
||||||
|
|
||||||
const response = await api.post<
|
const data: any = {
|
||||||
AxiosResponse<CreateTargetResponse>
|
|
||||||
>(`/target`, {
|
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
siteId: target.siteId,
|
siteId: target.siteId,
|
||||||
ip: target.ip,
|
ip: target.ip,
|
||||||
method: target.method,
|
method: target.method,
|
||||||
port: target.port,
|
port: target.port,
|
||||||
path: target.path,
|
|
||||||
pathMatchType: target.pathMatchType,
|
|
||||||
rewritePath: target.rewritePath,
|
|
||||||
rewritePathType: target.rewritePathType,
|
|
||||||
priority: target.priority,
|
|
||||||
enabled: target.enabled,
|
enabled: target.enabled,
|
||||||
hcEnabled: target.hcEnabled,
|
hcEnabled: target.hcEnabled,
|
||||||
hcPath: target.hcPath,
|
hcPath: target.hcPath,
|
||||||
hcInterval: target.hcInterval,
|
hcInterval: target.hcInterval,
|
||||||
hcTimeout: target.hcTimeout
|
hcTimeout: target.hcTimeout
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Only include path-related fields for HTTP resources
|
||||||
|
if (resource.http) {
|
||||||
|
data.path = target.path;
|
||||||
|
data.pathMatchType = target.pathMatchType;
|
||||||
|
data.rewritePath = target.rewritePath;
|
||||||
|
data.rewritePathType = target.rewritePathType;
|
||||||
|
data.priority = target.priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.post<
|
||||||
|
AxiosResponse<CreateTargetResponse>
|
||||||
|
>(`/target`, data);
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
// Update the target with the new ID and remove the new flag
|
// Update the target with the new ID and remove the new flag
|
||||||
@@ -615,19 +624,20 @@ export default function ReverseProxyTargets(props: {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
const site = sites.find((site) => site.siteId === data.siteId);
|
const site = sites.find((site) => site.siteId === data.siteId);
|
||||||
|
const isHttp = resource.http;
|
||||||
|
|
||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
...data,
|
...data,
|
||||||
path: data.path || null,
|
path: isHttp ? (data.path || null) : null,
|
||||||
pathMatchType: data.pathMatchType || null,
|
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
|
||||||
rewritePath: data.rewritePath || null,
|
rewritePath: isHttp ? (data.rewritePath || null) : null,
|
||||||
rewritePathType: data.rewritePathType || null,
|
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
|
||||||
siteType: site?.type || null,
|
siteType: site?.type || null,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
targetId: new Date().getTime(),
|
targetId: new Date().getTime(),
|
||||||
new: true,
|
new: true,
|
||||||
resourceId: resource.resourceId,
|
resourceId: resource.resourceId,
|
||||||
priority: 100,
|
priority: isHttp ? (data.priority || 100) : 100,
|
||||||
hcEnabled: false,
|
hcEnabled: false,
|
||||||
hcPath: null,
|
hcPath: null,
|
||||||
hcMethod: null,
|
hcMethod: null,
|
||||||
@@ -666,7 +676,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
...target,
|
...target,
|
||||||
...data,
|
...data,
|
||||||
updated: true,
|
updated: true,
|
||||||
// siteType: site?.type || null
|
siteType: site ? site.type : target.siteType
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
)
|
)
|
||||||
@@ -719,7 +729,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
// Save targets
|
// Save targets
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
const data = {
|
const data: any = {
|
||||||
ip: target.ip,
|
ip: target.ip,
|
||||||
port: target.port,
|
port: target.port,
|
||||||
method: target.method,
|
method: target.method,
|
||||||
@@ -735,14 +745,18 @@ export default function ReverseProxyTargets(props: {
|
|||||||
hcHeaders: target.hcHeaders || null,
|
hcHeaders: target.hcHeaders || null,
|
||||||
hcFollowRedirects: target.hcFollowRedirects || null,
|
hcFollowRedirects: target.hcFollowRedirects || null,
|
||||||
hcMethod: target.hcMethod || null,
|
hcMethod: target.hcMethod || null,
|
||||||
hcStatus: target.hcStatus || null,
|
hcStatus: target.hcStatus || null
|
||||||
path: target.path,
|
|
||||||
pathMatchType: target.pathMatchType,
|
|
||||||
rewritePath: target.rewritePath,
|
|
||||||
rewritePathType: target.rewritePathType,
|
|
||||||
priority: target.priority
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only include path-related fields for HTTP resources
|
||||||
|
if (resource.http) {
|
||||||
|
data.path = target.path;
|
||||||
|
data.pathMatchType = target.pathMatchType;
|
||||||
|
data.rewritePath = target.rewritePath;
|
||||||
|
data.rewritePathType = target.rewritePathType;
|
||||||
|
data.priority = target.priority;
|
||||||
|
}
|
||||||
|
|
||||||
if (target.new) {
|
if (target.new) {
|
||||||
const res = await api.put<
|
const res = await api.put<
|
||||||
AxiosResponse<CreateTargetResponse>
|
AxiosResponse<CreateTargetResponse>
|
||||||
@@ -814,6 +828,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
||||||
const baseColumns: ColumnDef<LocalTarget>[] = [];
|
const baseColumns: ColumnDef<LocalTarget>[] = [];
|
||||||
|
const isHttp = resource.http;
|
||||||
|
|
||||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||||
id: "priority",
|
id: "priority",
|
||||||
@@ -1007,14 +1022,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
) => {
|
) => {
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
...row.original,
|
...row.original,
|
||||||
ip: hostname
|
ip: hostname,
|
||||||
|
...(port && { port: port })
|
||||||
});
|
});
|
||||||
if (port) {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
port: port
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1051,12 +1061,12 @@ export default function ReverseProxyTargets(props: {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-[180px] justify-between text-sm font-medium border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
||||||
!row.original.siteId &&
|
!row.original.siteId &&
|
||||||
"text-muted-foreground"
|
"text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate max-w-[90px]">
|
<span className="truncate max-w-[150px]">
|
||||||
{row.original.siteId
|
{row.original.siteId
|
||||||
? selectedSite?.name
|
? selectedSite?.name
|
||||||
: t("siteSelect")}
|
: t("siteSelect")}
|
||||||
@@ -1133,7 +1143,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
<Input
|
<Input
|
||||||
defaultValue={row.original.ip}
|
defaultValue={row.original.ip}
|
||||||
placeholder="IP / Hostname"
|
placeholder="IP / Hostname"
|
||||||
className="flex-1 min-w-[120px] border-none placeholder-gray-400"
|
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
const input = e.target.value.trim();
|
const input = e.target.value.trim();
|
||||||
const hasProtocol =
|
const hasProtocol =
|
||||||
@@ -1317,15 +1327,20 @@ export default function ReverseProxyTargets(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isAdvancedMode) {
|
if (isAdvancedMode) {
|
||||||
return [
|
const columns = [
|
||||||
matchPathColumn,
|
|
||||||
addressColumn,
|
addressColumn,
|
||||||
rewritePathColumn,
|
|
||||||
priorityColumn,
|
|
||||||
healthCheckColumn,
|
healthCheckColumn,
|
||||||
enabledColumn,
|
enabledColumn,
|
||||||
actionsColumn
|
actionsColumn
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Only include path-related columns for HTTP resources
|
||||||
|
if (isHttp) {
|
||||||
|
columns.unshift(matchPathColumn);
|
||||||
|
columns.splice(3, 0, rewritePathColumn, priorityColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
addressColumn,
|
addressColumn,
|
||||||
@@ -1454,7 +1469,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="advanced-mode-toggle"
|
htmlFor="advanced-mode-toggle"
|
||||||
className="text-sm font-medium"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
{t("advancedMode")}
|
{t("advancedMode")}
|
||||||
</label>
|
</label>
|
||||||
@@ -1496,7 +1511,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="tls-settings-form"
|
id="tls-settings-form"
|
||||||
>
|
>
|
||||||
{build == "oss" && (
|
{!env.flags.usePangolinDns && (
|
||||||
<FormField
|
<FormField
|
||||||
control={tlsSettingsForm.control}
|
control={tlsSettingsForm.control}
|
||||||
name="ssl"
|
name="ssl"
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ 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";
|
||||||
@@ -58,7 +57,16 @@ import {
|
|||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { ArrowRight, CircleCheck, CircleX, Info, MoveRight, Plus, Settings, SquareArrowOutUpRight } from "lucide-react";
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
CircleCheck,
|
||||||
|
CircleX,
|
||||||
|
Info,
|
||||||
|
MoveRight,
|
||||||
|
Plus,
|
||||||
|
Settings,
|
||||||
|
SquareArrowOutUpRight
|
||||||
|
} from "lucide-react";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
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";
|
||||||
@@ -89,16 +97,25 @@ import { isTargetValid } from "@server/lib/validators";
|
|||||||
import { ListTargetsResponse } from "@server/routers/target";
|
import { ListTargetsResponse } from "@server/routers/target";
|
||||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
||||||
import { toASCII, toUnicode } from 'punycode';
|
import { toASCII, toUnicode } from "punycode";
|
||||||
import { DomainRow } from "../../../../../components/DomainsTable";
|
import { DomainRow } from "../../../../../components/DomainsTable";
|
||||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
import {
|
||||||
import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal";
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger
|
||||||
|
} from "@app/components/ui/tooltip";
|
||||||
|
import {
|
||||||
|
PathMatchDisplay,
|
||||||
|
PathMatchModal,
|
||||||
|
PathRewriteDisplay,
|
||||||
|
PathRewriteModal
|
||||||
|
} from "@app/components/PathMatchRenameModal";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import HealthCheckDialog from "@app/components/HealthCheckDialog";
|
import HealthCheckDialog from "@app/components/HealthCheckDialog";
|
||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
|
|
||||||
|
|
||||||
const baseResourceFormSchema = z.object({
|
const baseResourceFormSchema = z.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
http: z.boolean()
|
http: z.boolean()
|
||||||
@@ -115,54 +132,57 @@ const tcpUdpResourceFormSchema = z.object({
|
|||||||
// enableProxy: z.boolean().default(false)
|
// enableProxy: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetsSettingsSchema = z.object({
|
const addTargetSchema = z
|
||||||
stickySession: z.boolean()
|
.object({
|
||||||
});
|
ip: z.string().refine(isTargetValid),
|
||||||
|
method: z.string().nullable(),
|
||||||
|
port: z.coerce.number().int().positive(),
|
||||||
const addTargetSchema = z.object({
|
siteId: z.number().int().positive(),
|
||||||
ip: z.string().refine(isTargetValid),
|
path: z.string().optional().nullable(),
|
||||||
method: z.string().nullable(),
|
pathMatchType: z
|
||||||
port: z.coerce.number().int().positive(),
|
.enum(["exact", "prefix", "regex"])
|
||||||
siteId: z.number().int().positive(),
|
.optional()
|
||||||
path: z.string().optional().nullable(),
|
.nullable(),
|
||||||
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
rewritePath: z.string().optional().nullable(),
|
||||||
rewritePath: z.string().optional().nullable(),
|
rewritePathType: z
|
||||||
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
|
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||||
priority: z.number().int().min(1).max(1000)
|
.optional()
|
||||||
}).refine(
|
.nullable(),
|
||||||
(data) => {
|
priority: z.number().int().min(1).max(1000).optional()
|
||||||
// If path is provided, pathMatchType must be provided
|
})
|
||||||
if (data.path && !data.pathMatchType) {
|
.refine(
|
||||||
return false;
|
(data) => {
|
||||||
}
|
// If path is provided, pathMatchType must be provided
|
||||||
// If pathMatchType is provided, path must be provided
|
if (data.path && !data.pathMatchType) {
|
||||||
if (data.pathMatchType && !data.path) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Validate path based on pathMatchType
|
|
||||||
if (data.path && data.pathMatchType) {
|
|
||||||
switch (data.pathMatchType) {
|
|
||||||
case "exact":
|
|
||||||
case "prefix":
|
|
||||||
// Path should start with /
|
|
||||||
return data.path.startsWith("/");
|
|
||||||
case "regex":
|
|
||||||
// Validate regex
|
|
||||||
try {
|
|
||||||
new RegExp(data.path);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// If pathMatchType is provided, path must be provided
|
||||||
|
if (data.pathMatchType && !data.path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate path based on pathMatchType
|
||||||
|
if (data.path && data.pathMatchType) {
|
||||||
|
switch (data.pathMatchType) {
|
||||||
|
case "exact":
|
||||||
|
case "prefix":
|
||||||
|
// Path should start with /
|
||||||
|
return data.path.startsWith("/");
|
||||||
|
case "regex":
|
||||||
|
// Validate regex
|
||||||
|
try {
|
||||||
|
new RegExp(data.path);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Invalid path configuration"
|
||||||
}
|
}
|
||||||
return true;
|
)
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Invalid path configuration"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// If rewritePath is provided, rewritePathType must be provided
|
// If rewritePath is provided, rewritePathType must be provided
|
||||||
@@ -216,12 +236,14 @@ export default function Page() {
|
|||||||
>([]);
|
>([]);
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [showSnippets, setShowSnippets] = useState(false);
|
const [showSnippets, setShowSnippets] = useState(false);
|
||||||
const [resourceId, setResourceId] = useState<number | null>(null);
|
const [niceId, setNiceId] = useState<string>("");
|
||||||
|
|
||||||
// Target management state
|
// Target management state
|
||||||
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
||||||
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
|
||||||
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(new Map());
|
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
|
||||||
|
new Map()
|
||||||
|
);
|
||||||
|
|
||||||
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
|
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
|
||||||
useState<LocalTarget | null>(null);
|
useState<LocalTarget | null>(null);
|
||||||
@@ -246,17 +268,19 @@ export default function Page() {
|
|||||||
}, [isAdvancedMode]);
|
}, [isAdvancedMode]);
|
||||||
|
|
||||||
function addNewTarget() {
|
function addNewTarget() {
|
||||||
|
const isHttp = baseForm.watch("http");
|
||||||
|
|
||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
targetId: -Date.now(), // Use negative timestamp as temporary ID
|
targetId: -Date.now(), // Use negative timestamp as temporary ID
|
||||||
ip: "",
|
ip: "",
|
||||||
method: baseForm.watch("http") ? "http" : null,
|
method: isHttp ? "http" : null,
|
||||||
port: 0,
|
port: 0,
|
||||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
||||||
path: null,
|
path: isHttp ? null : null,
|
||||||
pathMatchType: null,
|
pathMatchType: isHttp ? null : null,
|
||||||
rewritePath: null,
|
rewritePath: isHttp ? null : null,
|
||||||
rewritePathType: null,
|
rewritePathType: isHttp ? null : null,
|
||||||
priority: 100,
|
priority: isHttp ? 100 : 100,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
resourceId: 0,
|
resourceId: 0,
|
||||||
hcEnabled: false,
|
hcEnabled: false,
|
||||||
@@ -290,12 +314,12 @@ export default function Page() {
|
|||||||
...(!env.flags.allowRawResources
|
...(!env.flags.allowRawResources
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
id: "raw" as ResourceType,
|
id: "raw" as ResourceType,
|
||||||
title: t("resourceRaw"),
|
title: t("resourceRaw"),
|
||||||
description: t("resourceRawDescription")
|
description: t("resourceRawDescription")
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
];
|
];
|
||||||
|
|
||||||
const baseForm = useForm({
|
const baseForm = useForm({
|
||||||
@@ -330,26 +354,39 @@ export default function Page() {
|
|||||||
pathMatchType: null,
|
pathMatchType: null,
|
||||||
rewritePath: null,
|
rewritePath: null,
|
||||||
rewritePathType: null,
|
rewritePathType: null,
|
||||||
priority: 100,
|
priority: baseForm.watch("http") ? 100 : undefined
|
||||||
} as z.infer<typeof addTargetSchema>
|
} as z.infer<typeof addTargetSchema>
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetsSettingsForm = useForm({
|
// Helper function to check if all targets have required fields using schema validation
|
||||||
resolver: zodResolver(targetsSettingsSchema),
|
const areAllTargetsValid = () => {
|
||||||
defaultValues: {
|
if (targets.length === 0) return true; // No targets is valid
|
||||||
stickySession: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const watchedIp = addTargetForm.watch("ip");
|
return targets.every((target) => {
|
||||||
const watchedPort = addTargetForm.watch("port");
|
try {
|
||||||
const watchedSiteId = addTargetForm.watch("siteId");
|
const isHttp = baseForm.watch("http");
|
||||||
|
const targetData: any = {
|
||||||
|
ip: target.ip,
|
||||||
|
method: target.method,
|
||||||
|
port: target.port,
|
||||||
|
siteId: target.siteId,
|
||||||
|
path: target.path,
|
||||||
|
pathMatchType: target.pathMatchType,
|
||||||
|
rewritePath: target.rewritePath,
|
||||||
|
rewritePathType: target.rewritePathType
|
||||||
|
};
|
||||||
|
|
||||||
const handleContainerSelect = (hostname: string, port?: number) => {
|
// Only include priority for HTTP resources
|
||||||
addTargetForm.setValue("ip", hostname);
|
if (isHttp) {
|
||||||
if (port) {
|
targetData.priority = target.priority;
|
||||||
addTargetForm.setValue("port", port);
|
}
|
||||||
}
|
|
||||||
|
addTargetSchema.parse(targetData);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializeDockerForSite = async (siteId: number) => {
|
const initializeDockerForSite = async (siteId: number) => {
|
||||||
@@ -360,14 +397,14 @@ export default function Page() {
|
|||||||
const dockerManager = new DockerManager(api, siteId);
|
const dockerManager = new DockerManager(api, siteId);
|
||||||
const dockerState = await dockerManager.initializeDocker();
|
const dockerState = await dockerManager.initializeDocker();
|
||||||
|
|
||||||
setDockerStates(prev => new Map(prev.set(siteId, dockerState)));
|
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshContainersForSite = async (siteId: number) => {
|
const refreshContainersForSite = async (siteId: number) => {
|
||||||
const dockerManager = new DockerManager(api, siteId);
|
const dockerManager = new DockerManager(api, siteId);
|
||||||
const containers = await dockerManager.fetchContainers();
|
const containers = await dockerManager.fetchContainers();
|
||||||
|
|
||||||
setDockerStates(prev => {
|
setDockerStates((prev) => {
|
||||||
const newMap = new Map(prev);
|
const newMap = new Map(prev);
|
||||||
const existingState = newMap.get(siteId);
|
const existingState = newMap.get(siteId);
|
||||||
if (existingState) {
|
if (existingState) {
|
||||||
@@ -378,11 +415,13 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getDockerStateForSite = (siteId: number): DockerState => {
|
const getDockerStateForSite = (siteId: number): DockerState => {
|
||||||
return dockerStates.get(siteId) || {
|
return (
|
||||||
isEnabled: false,
|
dockerStates.get(siteId) || {
|
||||||
isAvailable: false,
|
isEnabled: false,
|
||||||
containers: []
|
isAvailable: false,
|
||||||
};
|
containers: []
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function addTarget(data: z.infer<typeof addTargetSchema>) {
|
async function addTarget(data: z.infer<typeof addTargetSchema>) {
|
||||||
@@ -406,18 +445,20 @@ export default function Page() {
|
|||||||
|
|
||||||
const site = sites.find((site) => site.siteId === data.siteId);
|
const site = sites.find((site) => site.siteId === data.siteId);
|
||||||
|
|
||||||
|
const isHttp = baseForm.watch("http");
|
||||||
|
|
||||||
const newTarget: LocalTarget = {
|
const newTarget: LocalTarget = {
|
||||||
...data,
|
...data,
|
||||||
path: data.path || null,
|
path: isHttp ? (data.path || null) : null,
|
||||||
pathMatchType: data.pathMatchType || null,
|
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
|
||||||
rewritePath: data.rewritePath || null,
|
rewritePath: isHttp ? (data.rewritePath || null) : null,
|
||||||
rewritePathType: data.rewritePathType || null,
|
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
|
||||||
siteType: site?.type || null,
|
siteType: site?.type || null,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
targetId: new Date().getTime(),
|
targetId: new Date().getTime(),
|
||||||
new: true,
|
new: true,
|
||||||
resourceId: 0, // Will be set when resource is created
|
resourceId: 0, // Will be set when resource is created
|
||||||
priority: 100, // Default priority
|
priority: isHttp ? (data.priority || 100) : 100, // Default priority
|
||||||
hcEnabled: false,
|
hcEnabled: false,
|
||||||
hcPath: null,
|
hcPath: null,
|
||||||
hcMethod: null,
|
hcMethod: null,
|
||||||
@@ -443,7 +484,7 @@ export default function Page() {
|
|||||||
pathMatchType: null,
|
pathMatchType: null,
|
||||||
rewritePath: null,
|
rewritePath: null,
|
||||||
rewritePathType: null,
|
rewritePathType: null,
|
||||||
priority: 100,
|
priority: isHttp ? 100 : undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,11 +504,11 @@ export default function Page() {
|
|||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? {
|
? {
|
||||||
...target,
|
...target,
|
||||||
...data,
|
...data,
|
||||||
updated: true,
|
updated: true,
|
||||||
siteType: site?.type || null
|
siteType: site ? site.type : target.siteType
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -478,13 +519,11 @@ export default function Page() {
|
|||||||
|
|
||||||
const baseData = baseForm.getValues();
|
const baseData = baseForm.getValues();
|
||||||
const isHttp = baseData.http;
|
const isHttp = baseData.http;
|
||||||
const stickySessionData = targetsSettingsForm.getValues();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: baseData.name,
|
name: baseData.name,
|
||||||
http: baseData.http,
|
http: baseData.http,
|
||||||
stickySession: stickySessionData.stickySession
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let sanitizedSubdomain: string | undefined;
|
let sanitizedSubdomain: string | undefined;
|
||||||
@@ -497,7 +536,9 @@ export default function Page() {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
Object.assign(payload, {
|
Object.assign(payload, {
|
||||||
subdomain: sanitizedSubdomain ? toASCII(sanitizedSubdomain) : undefined,
|
subdomain: sanitizedSubdomain
|
||||||
|
? toASCII(sanitizedSubdomain)
|
||||||
|
: undefined,
|
||||||
domainId: httpData.domainId,
|
domainId: httpData.domainId,
|
||||||
protocol: "tcp"
|
protocol: "tcp"
|
||||||
});
|
});
|
||||||
@@ -528,13 +569,13 @@ export default function Page() {
|
|||||||
if (res && res.status === 201) {
|
if (res && res.status === 201) {
|
||||||
const id = res.data.data.resourceId;
|
const id = res.data.data.resourceId;
|
||||||
const niceId = res.data.data.niceId;
|
const niceId = res.data.data.niceId;
|
||||||
setResourceId(id);
|
setNiceId(niceId);
|
||||||
|
|
||||||
// Create targets if any exist
|
// Create targets if any exist
|
||||||
if (targets.length > 0) {
|
if (targets.length > 0) {
|
||||||
try {
|
try {
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
const data = {
|
const data: any = {
|
||||||
ip: target.ip,
|
ip: target.ip,
|
||||||
port: target.port,
|
port: target.port,
|
||||||
method: target.method,
|
method: target.method,
|
||||||
@@ -551,14 +592,18 @@ export default function Page() {
|
|||||||
hcPort: target.hcPort || null,
|
hcPort: target.hcPort || null,
|
||||||
hcFollowRedirects:
|
hcFollowRedirects:
|
||||||
target.hcFollowRedirects || null,
|
target.hcFollowRedirects || null,
|
||||||
hcStatus: target.hcStatus || null,
|
hcStatus: target.hcStatus || null
|
||||||
path: target.path,
|
|
||||||
pathMatchType: target.pathMatchType,
|
|
||||||
rewritePath: target.rewritePath,
|
|
||||||
rewritePathType: target.rewritePathType,
|
|
||||||
priority: target.priority
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only include path-related fields for HTTP resources
|
||||||
|
if (isHttp) {
|
||||||
|
data.path = target.path;
|
||||||
|
data.pathMatchType = target.pathMatchType;
|
||||||
|
data.rewritePath = target.rewritePath;
|
||||||
|
data.rewritePathType = target.rewritePathType;
|
||||||
|
data.priority = target.priority;
|
||||||
|
}
|
||||||
|
|
||||||
await api.put(`/resource/${id}/target`, data);
|
await api.put(`/resource/${id}/target`, data);
|
||||||
}
|
}
|
||||||
} catch (targetError) {
|
} catch (targetError) {
|
||||||
@@ -660,7 +705,7 @@ export default function Page() {
|
|||||||
const rawDomains = res.data.data.domains as DomainRow[];
|
const rawDomains = res.data.data.domains as DomainRow[];
|
||||||
const domains = rawDomains.map((domain) => ({
|
const domains = rawDomains.map((domain) => ({
|
||||||
...domain,
|
...domain,
|
||||||
baseDomain: toUnicode(domain.baseDomain),
|
baseDomain: toUnicode(domain.baseDomain)
|
||||||
}));
|
}));
|
||||||
setBaseDomains(domains);
|
setBaseDomains(domains);
|
||||||
// if (domains.length) {
|
// if (domains.length) {
|
||||||
@@ -683,10 +728,10 @@ export default function Page() {
|
|||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? {
|
? {
|
||||||
...target,
|
...target,
|
||||||
...config,
|
...config,
|
||||||
updated: true
|
updated: true
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -700,6 +745,7 @@ export default function Page() {
|
|||||||
|
|
||||||
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
||||||
const baseColumns: ColumnDef<LocalTarget>[] = [];
|
const baseColumns: ColumnDef<LocalTarget>[] = [];
|
||||||
|
const isHttp = baseForm.watch("http");
|
||||||
|
|
||||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||||
id: "priority",
|
id: "priority",
|
||||||
@@ -712,9 +758,7 @@ export default function Page() {
|
|||||||
<Info className="h-4 w-4 text-muted-foreground" />
|
<Info className="h-4 w-4 text-muted-foreground" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-xs">
|
<TooltipContent className="max-w-xs">
|
||||||
<p>
|
<p>{t("priorityDescription")}</p>
|
||||||
{t("priorityDescription")}
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
@@ -895,31 +939,51 @@ export default function Page() {
|
|||||||
) => {
|
) => {
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
...row.original,
|
...row.original,
|
||||||
ip: hostname
|
ip: hostname,
|
||||||
|
...(port && { port: port })
|
||||||
});
|
});
|
||||||
if (port) {
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
port: port
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input shadow-2xs rounded-md">
|
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input shadow-2xs rounded-md">
|
||||||
|
{selectedSite &&
|
||||||
|
selectedSite.type === "newt" &&
|
||||||
|
(() => {
|
||||||
|
const dockerState = getDockerStateForSite(
|
||||||
|
selectedSite.siteId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<ContainersSelector
|
||||||
|
site={selectedSite}
|
||||||
|
containers={dockerState.containers}
|
||||||
|
isAvailable={
|
||||||
|
dockerState.isAvailable
|
||||||
|
}
|
||||||
|
onContainerSelect={
|
||||||
|
handleContainerSelectForTarget
|
||||||
|
}
|
||||||
|
onRefresh={() =>
|
||||||
|
refreshContainersForSite(
|
||||||
|
selectedSite.siteId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
role="combobox"
|
role="combobox"
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-[180px] justify-between text-sm font-medium border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
||||||
!row.original.siteId &&
|
!row.original.siteId &&
|
||||||
"text-muted-foreground"
|
"text-muted-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate max-w-[90px]">
|
<span className="truncate max-w-[150px]">
|
||||||
{row.original.siteId
|
{row.original.siteId
|
||||||
? selectedSite?.name
|
? selectedSite?.name
|
||||||
: t("siteSelect")}
|
: t("siteSelect")}
|
||||||
@@ -969,30 +1033,6 @@ export default function Page() {
|
|||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
{selectedSite &&
|
|
||||||
selectedSite.type === "newt" &&
|
|
||||||
(() => {
|
|
||||||
const dockerState = getDockerStateForSite(
|
|
||||||
selectedSite.siteId
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<ContainersSelector
|
|
||||||
site={selectedSite}
|
|
||||||
containers={dockerState.containers}
|
|
||||||
isAvailable={
|
|
||||||
dockerState.isAvailable
|
|
||||||
}
|
|
||||||
onContainerSelect={
|
|
||||||
handleContainerSelectForTarget
|
|
||||||
}
|
|
||||||
onRefresh={() =>
|
|
||||||
refreshContainersForSite(
|
|
||||||
selectedSite.siteId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
defaultValue={row.original.method ?? "http"}
|
defaultValue={row.original.method ?? "http"}
|
||||||
@@ -1003,7 +1043,7 @@ export default function Page() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="h-8 px-2 w-[70px] text-sm font-normal border-none bg-transparent shadow-none focus:ring-0 focus:outline-none focus-visible:ring-0 data-[state=open]:bg-transparent">
|
<SelectTrigger className="h-8 px-2 w-[70px] border-none bg-transparent shadow-none focus:ring-0 focus:outline-none focus-visible:ring-0 data-[state=open]:bg-transparent">
|
||||||
{row.original.method || "http"}
|
{row.original.method || "http"}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -1020,7 +1060,7 @@ export default function Page() {
|
|||||||
<Input
|
<Input
|
||||||
defaultValue={row.original.ip}
|
defaultValue={row.original.ip}
|
||||||
placeholder="IP / Hostname"
|
placeholder="IP / Hostname"
|
||||||
className="flex-1 min-w-[120px] border-none placeholder-gray-400"
|
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
const input = e.target.value.trim();
|
const input = e.target.value.trim();
|
||||||
const hasProtocol =
|
const hasProtocol =
|
||||||
@@ -1204,15 +1244,20 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isAdvancedMode) {
|
if (isAdvancedMode) {
|
||||||
return [
|
const columns = [
|
||||||
matchPathColumn,
|
|
||||||
addressColumn,
|
addressColumn,
|
||||||
rewritePathColumn,
|
|
||||||
priorityColumn,
|
|
||||||
healthCheckColumn,
|
healthCheckColumn,
|
||||||
enabledColumn,
|
enabledColumn,
|
||||||
actionsColumn
|
actionsColumn
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Only include path-related columns for HTTP resources
|
||||||
|
if (isHttp) {
|
||||||
|
columns.unshift(matchPathColumn);
|
||||||
|
columns.splice(3, 0, rewritePathColumn, priorityColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns;
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
addressColumn,
|
addressColumn,
|
||||||
@@ -1464,10 +1509,10 @@ export default function Page() {
|
|||||||
.target
|
.target
|
||||||
.value
|
.value
|
||||||
? parseInt(
|
? parseInt(
|
||||||
e
|
e
|
||||||
.target
|
.target
|
||||||
.value
|
.value
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1546,60 +1591,87 @@ export default function Page() {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table
|
{table
|
||||||
.getHeaderGroups()
|
.getHeaderGroups()
|
||||||
.map((headerGroup) => (
|
.map(
|
||||||
<TableRow key={headerGroup.id}>
|
(
|
||||||
{headerGroup.headers.map(
|
headerGroup
|
||||||
(header) => (
|
) => (
|
||||||
<TableHead
|
<TableRow
|
||||||
key={header.id}
|
key={
|
||||||
>
|
headerGroup.id
|
||||||
{header.isPlaceholder
|
}
|
||||||
? null
|
>
|
||||||
: flexRender(
|
{headerGroup.headers.map(
|
||||||
header
|
(
|
||||||
.column
|
header
|
||||||
.columnDef
|
) => (
|
||||||
.header,
|
<TableHead
|
||||||
header.getContext()
|
|
||||||
)}
|
|
||||||
</TableHead>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{table.getRowModel().rows?.length ? (
|
|
||||||
table
|
|
||||||
.getRowModel()
|
|
||||||
.rows.map((row) => (
|
|
||||||
<TableRow key={row.id}>
|
|
||||||
{row
|
|
||||||
.getVisibleCells()
|
|
||||||
.map((cell) => (
|
|
||||||
<TableCell
|
|
||||||
key={
|
key={
|
||||||
cell.id
|
header.id
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{header.isPlaceholder
|
||||||
cell
|
? null
|
||||||
.column
|
: flexRender(
|
||||||
.columnDef
|
header
|
||||||
.cell,
|
.column
|
||||||
cell.getContext()
|
.columnDef
|
||||||
)}
|
.header,
|
||||||
</TableCell>
|
header.getContext()
|
||||||
))}
|
)}
|
||||||
|
</TableHead>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
)
|
||||||
|
)}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel()
|
||||||
|
.rows?.length ? (
|
||||||
|
table
|
||||||
|
.getRowModel()
|
||||||
|
.rows.map(
|
||||||
|
(row) => (
|
||||||
|
<TableRow
|
||||||
|
key={
|
||||||
|
row.id
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row
|
||||||
|
.getVisibleCells()
|
||||||
|
.map(
|
||||||
|
(
|
||||||
|
cell
|
||||||
|
) => (
|
||||||
|
<TableCell
|
||||||
|
key={
|
||||||
|
cell.id
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
|
cell
|
||||||
|
.column
|
||||||
|
.columnDef
|
||||||
|
.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
)
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={columns.length}
|
colSpan={
|
||||||
|
columns.length
|
||||||
|
}
|
||||||
className="h-24 text-center"
|
className="h-24 text-center"
|
||||||
>
|
>
|
||||||
{t("targetNoOne")}
|
{t(
|
||||||
|
"targetNoOne"
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@@ -1621,12 +1693,16 @@ export default function Page() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
id="advanced-mode-toggle"
|
id="advanced-mode-toggle"
|
||||||
checked={isAdvancedMode}
|
checked={
|
||||||
onCheckedChange={setIsAdvancedMode}
|
isAdvancedMode
|
||||||
|
}
|
||||||
|
onCheckedChange={
|
||||||
|
setIsAdvancedMode
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
htmlFor="advanced-mode-toggle"
|
htmlFor="advanced-mode-toggle"
|
||||||
className="text-sm font-medium"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
{t("advancedMode")}
|
{t("advancedMode")}
|
||||||
</label>
|
</label>
|
||||||
@@ -1639,7 +1715,10 @@ export default function Page() {
|
|||||||
<p className="text-muted-foreground mb-4">
|
<p className="text-muted-foreground mb-4">
|
||||||
{t("targetNoOne")}
|
{t("targetNoOne")}
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={addNewTarget} variant="outline">
|
<Button
|
||||||
|
onClick={addNewTarget}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{t("addTarget")}
|
{t("addTarget")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1677,6 +1756,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={createLoading}
|
loading={createLoading}
|
||||||
|
disabled={!areAllTargetsValid()}
|
||||||
>
|
>
|
||||||
{t("resourceCreate")}
|
{t("resourceCreate")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1685,24 +1765,36 @@ export default function Page() {
|
|||||||
<HealthCheckDialog
|
<HealthCheckDialog
|
||||||
open={healthCheckDialogOpen}
|
open={healthCheckDialogOpen}
|
||||||
setOpen={setHealthCheckDialogOpen}
|
setOpen={setHealthCheckDialogOpen}
|
||||||
targetId={selectedTargetForHealthCheck.targetId}
|
targetId={
|
||||||
|
selectedTargetForHealthCheck.targetId
|
||||||
|
}
|
||||||
targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`}
|
targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`}
|
||||||
targetMethod={
|
targetMethod={
|
||||||
selectedTargetForHealthCheck.method || undefined
|
selectedTargetForHealthCheck.method ||
|
||||||
|
undefined
|
||||||
}
|
}
|
||||||
initialConfig={{
|
initialConfig={{
|
||||||
hcEnabled:
|
hcEnabled:
|
||||||
selectedTargetForHealthCheck.hcEnabled || false,
|
selectedTargetForHealthCheck.hcEnabled ||
|
||||||
hcPath: selectedTargetForHealthCheck.hcPath || "/",
|
false,
|
||||||
|
hcPath:
|
||||||
|
selectedTargetForHealthCheck.hcPath ||
|
||||||
|
"/",
|
||||||
hcMethod:
|
hcMethod:
|
||||||
selectedTargetForHealthCheck.hcMethod || "GET",
|
selectedTargetForHealthCheck.hcMethod ||
|
||||||
|
"GET",
|
||||||
hcInterval:
|
hcInterval:
|
||||||
selectedTargetForHealthCheck.hcInterval || 5,
|
selectedTargetForHealthCheck.hcInterval ||
|
||||||
hcTimeout: selectedTargetForHealthCheck.hcTimeout || 5,
|
5,
|
||||||
|
hcTimeout:
|
||||||
|
selectedTargetForHealthCheck.hcTimeout ||
|
||||||
|
5,
|
||||||
hcHeaders:
|
hcHeaders:
|
||||||
selectedTargetForHealthCheck.hcHeaders || undefined,
|
selectedTargetForHealthCheck.hcHeaders ||
|
||||||
|
undefined,
|
||||||
hcScheme:
|
hcScheme:
|
||||||
selectedTargetForHealthCheck.hcScheme || undefined,
|
selectedTargetForHealthCheck.hcScheme ||
|
||||||
|
undefined,
|
||||||
hcHostname:
|
hcHostname:
|
||||||
selectedTargetForHealthCheck.hcHostname ||
|
selectedTargetForHealthCheck.hcHostname ||
|
||||||
selectedTargetForHealthCheck.ip,
|
selectedTargetForHealthCheck.ip,
|
||||||
@@ -1713,8 +1805,11 @@ export default function Page() {
|
|||||||
selectedTargetForHealthCheck.hcFollowRedirects ||
|
selectedTargetForHealthCheck.hcFollowRedirects ||
|
||||||
true,
|
true,
|
||||||
hcStatus:
|
hcStatus:
|
||||||
selectedTargetForHealthCheck.hcStatus || undefined,
|
selectedTargetForHealthCheck.hcStatus ||
|
||||||
hcMode: selectedTargetForHealthCheck.hcMode || "http",
|
undefined,
|
||||||
|
hcMode:
|
||||||
|
selectedTargetForHealthCheck.hcMode ||
|
||||||
|
"http",
|
||||||
hcUnhealthyInterval:
|
hcUnhealthyInterval:
|
||||||
selectedTargetForHealthCheck.hcUnhealthyInterval ||
|
selectedTargetForHealthCheck.hcUnhealthyInterval ||
|
||||||
30
|
30
|
||||||
@@ -1749,7 +1844,9 @@ export default function Page() {
|
|||||||
{t("resourceAddEntrypoints")}
|
{t("resourceAddEntrypoints")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("resourceAddEntrypointsEditFile")}
|
{t(
|
||||||
|
"resourceAddEntrypointsEditFile"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<CopyTextBox
|
<CopyTextBox
|
||||||
text={`entryPoints:
|
text={`entryPoints:
|
||||||
@@ -1764,7 +1861,9 @@ export default function Page() {
|
|||||||
{t("resourceExposePorts")}
|
{t("resourceExposePorts")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t("resourceExposePortsEditFile")}
|
{t(
|
||||||
|
"resourceExposePortsEditFile"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<CopyTextBox
|
<CopyTextBox
|
||||||
text={`ports:
|
text={`ports:
|
||||||
@@ -1802,7 +1901,7 @@ export default function Page() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(
|
||||||
`/${orgId}/settings/resources/${resourceId}/proxy`
|
`/${orgId}/settings/resources/${niceId}/proxy`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ export default async function RootLayout({
|
|||||||
)
|
)
|
||||||
)();
|
)();
|
||||||
licenseStatus = licenseStatusRes.data.data;
|
licenseStatus = licenseStatusRes.data.data;
|
||||||
|
} else if (build === "saas") {
|
||||||
|
licenseStatus = {
|
||||||
|
isHostLicensed: true,
|
||||||
|
isLicenseValid: true,
|
||||||
|
hostId: "saas"
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
licenseStatus = {
|
licenseStatus = {
|
||||||
isHostLicensed: false,
|
isHostLicensed: false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -13,6 +14,7 @@ type BrandingLogoProps = {
|
|||||||
export default function BrandingLogo(props: BrandingLogoProps) {
|
export default function BrandingLogo(props: BrandingLogoProps) {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
const [path, setPath] = useState<string>(""); // Default logo path
|
const [path, setPath] = useState<string>(""); // Default logo path
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,12 +29,16 @@ export default function BrandingLogo(props: BrandingLogoProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lightOrDark === "light") {
|
if (lightOrDark === "light") {
|
||||||
return (
|
if (isUnlocked() && env.branding.logo?.lightPath) {
|
||||||
env.branding.logo?.lightPath || "/logo/word_mark_black.png"
|
return env.branding.logo.lightPath;
|
||||||
);
|
}
|
||||||
|
return "/logo/word_mark_black.png";
|
||||||
}
|
}
|
||||||
|
|
||||||
return env.branding.logo?.darkPath || "/logo/word_mark_white.png";
|
if (isUnlocked() && env.branding.logo?.darkPath) {
|
||||||
|
return env.branding.logo.darkPath;
|
||||||
|
}
|
||||||
|
return "/logo/word_mark_white.png";
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = getPath();
|
const path = getPath();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Image from "next/image";
|
|||||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
import BrandingLogo from "@app/components/BrandingLogo";
|
import BrandingLogo from "@app/components/BrandingLogo";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
|
||||||
type DashboardLoginFormProps = {
|
type DashboardLoginFormProps = {
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
@@ -29,18 +30,22 @@ export default function DashboardLoginForm({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
|
||||||
function getSubtitle() {
|
function getSubtitle() {
|
||||||
return t("loginStart");
|
return t("loginStart");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 175 : 175;
|
||||||
|
const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 58 : 58;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="shadow-md w-full max-w-md">
|
<Card className="shadow-md w-full max-w-md">
|
||||||
<CardHeader className="border-b">
|
<CardHeader className="border-b">
|
||||||
<div className="flex flex-row items-center justify-center">
|
<div className="flex flex-row items-center justify-center">
|
||||||
<BrandingLogo
|
<BrandingLogo
|
||||||
height={env.branding.logo?.authPage?.height || 58}
|
height={logoHeight}
|
||||||
width={env.branding.logo?.authPage?.width || 175}
|
width={logoWidth}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center space-y-1 pt-3">
|
<div className="text-center space-y-1 pt-3">
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ProfileIcon from "@app/components/ProfileIcon";
|
import ProfileIcon from "@app/components/ProfileIcon";
|
||||||
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import BrandingLogo from "./BrandingLogo";
|
import BrandingLogo from "./BrandingLogo";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { Badge } from "./ui/badge";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
interface LayoutHeaderProps {
|
interface LayoutHeaderProps {
|
||||||
showTopBar: boolean;
|
showTopBar: boolean;
|
||||||
@@ -19,6 +17,14 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
|
|||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const [path, setPath] = useState<string>("");
|
const [path, setPath] = useState<string>("");
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
|
||||||
|
const logoWidth = isUnlocked()
|
||||||
|
? env.branding.logo?.navbar?.width || 98
|
||||||
|
: 98;
|
||||||
|
const logoHeight = isUnlocked()
|
||||||
|
? env.branding.logo?.navbar?.height || 32
|
||||||
|
: 32;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function getPath() {
|
function getPath() {
|
||||||
@@ -50,12 +56,8 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/" className="flex items-center">
|
<Link href="/" className="flex items-center">
|
||||||
<BrandingLogo
|
<BrandingLogo
|
||||||
width={
|
width={logoWidth}
|
||||||
env.branding.logo?.navbar?.width || 98
|
height={logoHeight}
|
||||||
}
|
|
||||||
height={
|
|
||||||
env.branding.logo?.navbar?.height || 32
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
{/* {build === "saas" && (
|
{/* {build === "saas" && (
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ export function LayoutSidebar({
|
|||||||
}, [isSidebarCollapsed]);
|
}, [isSidebarCollapsed]);
|
||||||
|
|
||||||
function loadFooterLinks(): { text: string; href?: string }[] | undefined {
|
function loadFooterLinks(): { text: string; href?: string }[] | undefined {
|
||||||
|
if (!isUnlocked()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (env.branding.footer) {
|
if (env.branding.footer) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(env.branding.footer);
|
return JSON.parse(env.branding.footer);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import BrandingLogo from "@app/components/BrandingLogo";
|
|||||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
|
||||||
const pinSchema = z.object({
|
const pinSchema = z.object({
|
||||||
pin: z
|
pin: z
|
||||||
@@ -92,6 +93,7 @@ type ResourceAuthPortalProps = {
|
|||||||
export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
|
||||||
const getNumMethods = () => {
|
const getNumMethods = () => {
|
||||||
let colLength = 0;
|
let colLength = 0;
|
||||||
@@ -308,14 +310,22 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTitle() {
|
function getTitle() {
|
||||||
if (build !== "oss" && env.branding.resourceAuthPage?.titleText) {
|
if (
|
||||||
|
isUnlocked() &&
|
||||||
|
build !== "oss" &&
|
||||||
|
env.branding.resourceAuthPage?.titleText
|
||||||
|
) {
|
||||||
return env.branding.resourceAuthPage.titleText;
|
return env.branding.resourceAuthPage.titleText;
|
||||||
}
|
}
|
||||||
return t("authenticationRequired");
|
return t("authenticationRequired");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubtitle(resourceName: string) {
|
function getSubtitle(resourceName: string) {
|
||||||
if (build !== "oss" && env.branding.resourceAuthPage?.subtitleText) {
|
if (
|
||||||
|
isUnlocked() &&
|
||||||
|
build !== "oss" &&
|
||||||
|
env.branding.resourceAuthPage?.subtitleText
|
||||||
|
) {
|
||||||
return env.branding.resourceAuthPage.subtitleText
|
return env.branding.resourceAuthPage.subtitleText
|
||||||
.split("{{resourceName}}")
|
.split("{{resourceName}}")
|
||||||
.join(resourceName);
|
.join(resourceName);
|
||||||
@@ -325,11 +335,14 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
: t("authenticationRequest", { name: resourceName });
|
: t("authenticationRequest", { name: resourceName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 100 : 100;
|
||||||
|
const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 100 : 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!accessDenied ? (
|
{!accessDenied ? (
|
||||||
<div>
|
<div>
|
||||||
{build === "enterprise" ? (
|
{isUnlocked() && build === "enterprise" ? (
|
||||||
!env.branding.resourceAuthPage?.hidePoweredBy && (
|
!env.branding.resourceAuthPage?.hidePoweredBy && (
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
@@ -362,18 +375,13 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
|||||||
)}
|
)}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
{build !== "oss" &&
|
{isUnlocked() &&
|
||||||
|
build !== "oss" &&
|
||||||
env.branding?.resourceAuthPage?.showLogo && (
|
env.branding?.resourceAuthPage?.showLogo && (
|
||||||
<div className="flex flex-row items-center justify-center mb-3">
|
<div className="flex flex-row items-center justify-center mb-3">
|
||||||
<BrandingLogo
|
<BrandingLogo
|
||||||
height={
|
height={logoHeight}
|
||||||
env.branding.logo?.authPage
|
width={logoWidth}
|
||||||
?.height || 100
|
|
||||||
}
|
|
||||||
width={
|
|
||||||
env.branding.logo?.authPage
|
|
||||||
?.width || 100
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export default function SidebarLicenseButton({
|
|||||||
}: SidebarLicenseButtonProps) {
|
}: SidebarLicenseButtonProps) {
|
||||||
const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext();
|
const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext();
|
||||||
|
|
||||||
|
const url = "https://docs.digpangolin.com/self-host/enterprise-edition";
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -30,21 +32,21 @@ export default function SidebarLicenseButton({
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Link href="https://docs.digpangolin.com/">
|
<Link href={url}>
|
||||||
<Button size="icon" className="w-8 h-8">
|
<Button size="icon" className="w-8 h-8">
|
||||||
<TicketCheck className="h-4 w-4" />
|
<TicketCheck className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" sideOffset={8}>
|
<TooltipContent side="right" sideOffset={8}>
|
||||||
Enable Enterprise License
|
{t("sidebarEnableEnterpriseLicense")}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
) : (
|
) : (
|
||||||
<Link href="https://docs.digpangolin.com/">
|
<Link href={url}>
|
||||||
<Button size="sm" className="gap-2 w-full">
|
<Button size="sm" className="gap-2 w-full">
|
||||||
Enable Enterprise License
|
{t("sidebarEnableEnterpriseLicense")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle
|
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
@@ -31,13 +29,13 @@ import { AxiosResponse } from "axios";
|
|||||||
import { formatAxiosError } from "@app/lib/api";
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import Image from "next/image";
|
|
||||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import BrandingLogo from "@app/components/BrandingLogo";
|
import BrandingLogo from "@app/components/BrandingLogo";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, X } from "lucide-react";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
|
||||||
// Password strength calculation
|
// Password strength calculation
|
||||||
const calculatePasswordStrength = (password: string) => {
|
const calculatePasswordStrength = (password: string) => {
|
||||||
@@ -111,6 +109,7 @@ export default function SignupForm({
|
|||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -192,14 +191,18 @@ export default function SignupForm({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logoWidth = isUnlocked()
|
||||||
|
? env.branding.logo?.authPage?.width || 175
|
||||||
|
: 175;
|
||||||
|
const logoHeight = isUnlocked()
|
||||||
|
? env.branding.logo?.authPage?.height || 58
|
||||||
|
: 58;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md shadow-md">
|
<Card className="w-full max-w-md shadow-md">
|
||||||
<CardHeader className="border-b">
|
<CardHeader className="border-b">
|
||||||
<div className="flex flex-row items-center justify-center">
|
<div className="flex flex-row items-center justify-center">
|
||||||
<BrandingLogo
|
<BrandingLogo height={logoHeight} width={logoWidth} />
|
||||||
height={env.branding.logo?.authPage?.height || 58}
|
|
||||||
width={env.branding.logo?.authPage?.width || 175}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center space-y-1 pt-3">
|
<div className="text-center space-y-1 pt-3">
|
||||||
<p className="text-muted-foreground">{getSubtitle()}</p>
|
<p className="text-muted-foreground">{getSubtitle()}</p>
|
||||||
|
|||||||
@@ -300,33 +300,65 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const originalRow = row.original;
|
const originalRow = row.original;
|
||||||
return (
|
return originalRow.exitNodeName ? (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<span>{originalRow.exitNodeName}</span>
|
<span>{originalRow.exitNodeName}</span>
|
||||||
{build == "saas" && originalRow.exitNodeName &&
|
{build == "saas" &&
|
||||||
['mercury', 'venus', 'earth', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune'].includes(originalRow.exitNodeName.toLowerCase()) && (
|
originalRow.exitNodeName &&
|
||||||
<Badge variant="secondary">Cloud</Badge>
|
[
|
||||||
)}
|
"mercury",
|
||||||
|
"venus",
|
||||||
|
"earth",
|
||||||
|
"mars",
|
||||||
|
"jupiter",
|
||||||
|
"saturn",
|
||||||
|
"uranus",
|
||||||
|
"neptune"
|
||||||
|
].includes(
|
||||||
|
originalRow.exitNodeName.toLowerCase()
|
||||||
|
) && <Badge variant="secondary">Cloud</Badge>}
|
||||||
</div>
|
</div>
|
||||||
);
|
) : (
|
||||||
},
|
"-"
|
||||||
},
|
|
||||||
...(env.flags.enableClients ? [{
|
|
||||||
accessorKey: "address",
|
|
||||||
header: ({ column }: { column: Column<SiteRow, unknown> }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Address
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}] : []),
|
},
|
||||||
|
...(env.flags.enableClients
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
accessorKey: "address",
|
||||||
|
header: ({
|
||||||
|
column
|
||||||
|
}: {
|
||||||
|
column: Column<SiteRow, unknown>;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
column.toggleSorting(
|
||||||
|
column.getIsSorted() === "asc"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Address
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }: { row: any }) => {
|
||||||
|
const originalRow = row.original;
|
||||||
|
return originalRow.address ? (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span>{originalRow.address}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"-"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
|
|
||||||
type SplashImageProps = {
|
type SplashImageProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -11,8 +12,12 @@ type SplashImageProps = {
|
|||||||
export default function SplashImage({ children }: SplashImageProps) {
|
export default function SplashImage({ children }: SplashImageProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
|
const { isUnlocked } = useLicenseStatusContext();
|
||||||
|
|
||||||
function showBackgroundImage() {
|
function showBackgroundImage() {
|
||||||
|
if (!isUnlocked()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!env.branding.background_image_path) {
|
if (!env.branding.background_image_path) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import LicenseStatusContext from "@app/contexts/licenseStatusContext";
|
import LicenseStatusContext from "@app/contexts/licenseStatusContext";
|
||||||
|
import { build } from "@server/build";
|
||||||
import { LicenseStatus } from "@server/license/license";
|
import { LicenseStatus } from "@server/license/license";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user