mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-18 02:46:37 +00:00
Compare commits
78 Commits
1.11.0-s.3
...
1.11.1-s.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5bab6bb80 | ||
|
|
7536c03f63 | ||
|
|
ada5d2ef0e | ||
|
|
b8bead0590 | ||
|
|
68f852d6d1 | ||
|
|
d9fe5a8819 | ||
|
|
346183a23f | ||
|
|
dcfd7f5443 | ||
|
|
e59cd6672b | ||
|
|
7c8c440f67 | ||
|
|
f258c41f15 | ||
|
|
ae4a24f4aa | ||
|
|
476cdcfe86 | ||
|
|
f869df2f65 | ||
|
|
03cfabacd9 | ||
|
|
47ac5875f3 | ||
|
|
f67327358e | ||
|
|
4901823f15 | ||
|
|
5407e3c821 | ||
|
|
1d5cdad8b7 | ||
|
|
cd2424cb77 | ||
|
|
c17efde6bf | ||
|
|
40cd8cdec7 | ||
|
|
6768672a44 | ||
|
|
240c5b005b | ||
|
|
8dde170a35 | ||
|
|
c07abf8ff9 | ||
|
|
e5a436593f | ||
|
|
bb6e093ac6 | ||
|
|
59a334ce24 | ||
|
|
d241dcfb27 | ||
|
|
af263e7913 | ||
|
|
6610e7d405 | ||
|
|
c476e65cf2 | ||
|
|
b69b2eeeb3 | ||
|
|
89dab0917b | ||
|
|
73efdb95ae | ||
|
|
1bcca88614 | ||
|
|
3af1e0ef56 | ||
|
|
8387571c1d | ||
|
|
1d017f60b4 | ||
|
|
81effda9e8 | ||
|
|
9343906ab1 | ||
|
|
08b7d6735c | ||
|
|
a91ebd1e91 | ||
|
|
312e03b4eb | ||
|
|
e8a57e432c | ||
|
|
bca2eef2e8 | ||
|
|
ec7211a15d | ||
|
|
46807c6477 | ||
|
|
b578786e62 | ||
|
|
2e0ad8d262 | ||
|
|
003f0cfa6d | ||
|
|
ee3df081ef | ||
|
|
08eeb12519 | ||
|
|
e66c6b2505 | ||
|
|
d2a880d9c8 | ||
|
|
edc0b86470 | ||
|
|
aebe6b80b7 | ||
|
|
4d87333b43 | ||
|
|
ef32f3ed5a | ||
|
|
216ded3034 | ||
|
|
cb59fe2cee | ||
|
|
7776f6d09c | ||
|
|
c50392c947 | ||
|
|
ceee978fcd | ||
|
|
c5a73dc87e | ||
|
|
7198ef2774 | ||
|
|
7e9a066797 | ||
|
|
ba96332313 | ||
|
|
e2d0338b0b | ||
|
|
59ecab5738 | ||
|
|
721bf3403d | ||
|
|
3b8ba47377 | ||
|
|
e752929f69 | ||
|
|
e41c3e6f54 | ||
|
|
9dedd1a8de | ||
|
|
c4a5fae28f |
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: amd64-runner
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -4,7 +4,7 @@ Contributions are welcome!
|
||||
|
||||
Please see the contribution and local development guide on the docs page before getting started:
|
||||
|
||||
https://docs.digpangolin.com/development/contributing
|
||||
https://docs.pangolin.net/development/contributing
|
||||
|
||||
### Licensing Considerations
|
||||
|
||||
|
||||
22
Makefile
22
Makefile
@@ -8,7 +8,7 @@ build-release:
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:latest \
|
||||
@@ -17,7 +17,7 @@ build-release:
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-latest \
|
||||
@@ -25,6 +25,24 @@ build-release:
|
||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-latest \
|
||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push .
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64,linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(minor_tag) \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-arm:
|
||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||
|
||||
36
README.md
36
README.md
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
<h2>
|
||||
<a href="https://digpangolin.com">
|
||||
<a href="https://pangolin.net/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="public/logo/word_mark_white.png">
|
||||
<img alt="Pangolin Logo" src="public/logo/word_mark_black.png" width="350">
|
||||
@@ -11,15 +11,15 @@
|
||||
|
||||
<div align="center">
|
||||
<h5>
|
||||
<a href="https://digpangolin.com">
|
||||
<a href="https://pangolin.net/">
|
||||
Website
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.digpangolin.com/">
|
||||
<a href="https://docs.pangolin.net/">
|
||||
Documentation
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="mailto:contact@fossorial.io">
|
||||
<a href="mailto:contact@pangolin.net">
|
||||
Contact Us
|
||||
</a>
|
||||
</h5>
|
||||
@@ -28,26 +28,32 @@
|
||||
<div align="center">
|
||||
|
||||
[](https://discord.gg/HCJR8Xhme4)
|
||||
[](https://digpangolin.com/slack)
|
||||
[](https://pangolin.net/slack)
|
||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||

|
||||
[](https://www.youtube.com/@fossorial-app)
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<strong>
|
||||
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN.
|
||||
|
||||
## Installation
|
||||
|
||||
Check out the [quick install guide](https://docs.digpangolin.com) for how to install and set up Pangolin.
|
||||
Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin.
|
||||
|
||||
## Deployment Options
|
||||
|
||||
| <img width=500 /> | Description |
|
||||
|-----------------|--------------|
|
||||
| **Self-Host: Community Edition** | 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://github.com/fosrl/remote-note) 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.pangolin.net/manage/remote-node/nodes) and connect to our control plane. |
|
||||
|
||||
## Key Features
|
||||
|
||||
@@ -55,27 +61,27 @@ Pangolin packages everything you need for seamless application access and exposu
|
||||
|
||||
| <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> |
|
||||
| **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> |
|
||||
| **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> |
|
||||
| **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> |
|
||||
| **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" 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" 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" width=500 /><tr></tr> |
|
||||
|
||||
## Get Started
|
||||
|
||||
### Check out the docs
|
||||
|
||||
We encourage everyone to read the full documentation first, which is
|
||||
available at [docs.digpangolin.com](https://docs.digpangolin.com). This README provides only a very brief subset of
|
||||
available at [docs.pangolin.net](https://docs.pangolin.net). This README provides only a very brief subset of
|
||||
the docs to illustrate some basic ideas.
|
||||
|
||||
### Sign up and try now
|
||||
|
||||
For Pangolin's managed service, you will first need to create an account at
|
||||
[pangolin.fossorial.io](https://pangolin.fossorial.io). We have a generous free tier to get started.
|
||||
[app.pangolin.net](https://app.pangolin.net). We have a generous free tier to get started.
|
||||
|
||||
## Licensing
|
||||
|
||||
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://digpangolin.com/fcl.html). For inquiries about commercial licensing, please contact us at [contact@fossorial.io](mailto:contact@fossorial.io).
|
||||
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net).
|
||||
|
||||
## Contributions
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
|
||||
|
||||
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
|
||||
2. Send a detailed report to [security@fossorial.io](mailto:security@fossorial.io) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||
2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
|
||||
|
||||
- Description and location of the vulnerability.
|
||||
- Potential impact of the vulnerability.
|
||||
|
||||
@@ -8,7 +8,7 @@ import base64
|
||||
YAML_FILE_PATH = 'blueprint.yaml'
|
||||
|
||||
# The API endpoint and headers from the curl request
|
||||
API_URL = 'http://api.pangolin.fossorial.io/v1/org/test/blueprint'
|
||||
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
|
||||
HEADERS = {
|
||||
'accept': '*/*',
|
||||
'Authorization': 'Bearer <your_token_here>',
|
||||
|
||||
@@ -28,9 +28,9 @@ proxy-resources:
|
||||
# sso-roles:
|
||||
# - Member
|
||||
# sso-users:
|
||||
# - owen@fossorial.io
|
||||
# - owen@pangolin.net
|
||||
# whitelist-users:
|
||||
# - owen@fossorial.io
|
||||
# - owen@pangolin.net
|
||||
headers:
|
||||
- name: X-Example-Header
|
||||
value: example-value
|
||||
|
||||
@@ -12,7 +12,7 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "owen@fossorial.io",
|
||||
"email": "owen@pangolin.net",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ post {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "milo@fossorial.io"
|
||||
"email": "milo@pangolin.net"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ put {
|
||||
|
||||
body:json {
|
||||
{
|
||||
"email": "numbat@fossorial.io",
|
||||
"email": "numbat@pangolin.net",
|
||||
"password": "Password123!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
||||
|
||||
app:
|
||||
dashboard_url: http://localhost:3002
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.digpangolin.com/
|
||||
# https://docs.pangolin.net/
|
||||
|
||||
gerbil:
|
||||
start_port: 51820
|
||||
@@ -36,4 +36,4 @@ flags:
|
||||
require_email_verification: {{.EnableEmail}}
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
allow_raw_resources: true
|
||||
|
||||
@@ -6,8 +6,6 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config:/app/config
|
||||
- pangolin-data-certificates:/var/certificates
|
||||
- pangolin-data-dynamic:/var/dynamic
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||
interval: "10s"
|
||||
@@ -22,7 +20,7 @@ services:
|
||||
pangolin:
|
||||
condition: service_healthy
|
||||
command:
|
||||
- --reachableAt=http://gerbil:3003
|
||||
- --reachableAt=http://gerbil:3004
|
||||
- --generateAndSaveKeyTo=/var/config/key
|
||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
||||
volumes:
|
||||
@@ -56,16 +54,9 @@ services:
|
||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||
- ./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:
|
||||
default:
|
||||
driver: bridge
|
||||
name: pangolin
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
|
||||
volumes:
|
||||
pangolin-data-dynamic:
|
||||
pangolin-data-certificates:
|
||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||
180
install/get-installer.sh
Normal file
180
install/get-installer.sh
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Get installer - Cross-platform installation script
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/fosrl/installer/refs/heads/main/get-installer.sh | bash
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# GitHub repository info
|
||||
REPO="fosrl/pangolin"
|
||||
GITHUB_API_URL="https://api.github.com/repos/${REPO}/releases/latest"
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Function to get latest version from GitHub API
|
||||
get_latest_version() {
|
||||
local latest_info
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
latest_info=$(curl -fsSL "$GITHUB_API_URL" 2>/dev/null)
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
latest_info=$(wget -qO- "$GITHUB_API_URL" 2>/dev/null)
|
||||
else
|
||||
print_error "Neither curl nor wget is available. Please install one of them." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$latest_info" ]; then
|
||||
print_error "Failed to fetch latest version information" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract version from JSON response (works without jq)
|
||||
local version=$(echo "$latest_info" | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
print_error "Could not parse version from GitHub API response" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove 'v' prefix if present
|
||||
version=$(echo "$version" | sed 's/^v//')
|
||||
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
# Detect OS and architecture
|
||||
detect_platform() {
|
||||
local os arch
|
||||
|
||||
# Detect OS - only support Linux
|
||||
case "$(uname -s)" in
|
||||
Linux*) os="linux" ;;
|
||||
*)
|
||||
print_error "Unsupported operating system: $(uname -s). Only Linux is supported."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect architecture - only support amd64 and arm64
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) arch="amd64" ;;
|
||||
arm64|aarch64) arch="arm64" ;;
|
||||
*)
|
||||
print_error "Unsupported architecture: $(uname -m). Only amd64 and arm64 are supported on Linux."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "${os}_${arch}"
|
||||
}
|
||||
|
||||
# Get installation directory
|
||||
get_install_dir() {
|
||||
# Install to the current directory
|
||||
local install_dir="$(pwd)"
|
||||
if [ ! -d "$install_dir" ]; then
|
||||
print_error "Installation directory does not exist: $install_dir"
|
||||
exit 1
|
||||
fi
|
||||
echo "$install_dir"
|
||||
}
|
||||
|
||||
# Download and install installer
|
||||
install_installer() {
|
||||
local platform="$1"
|
||||
local install_dir="$2"
|
||||
local binary_name="installer_${platform}"
|
||||
|
||||
local download_url="${BASE_URL}/${binary_name}"
|
||||
local temp_file="/tmp/installer"
|
||||
local final_path="${install_dir}/installer"
|
||||
|
||||
print_status "Downloading installer from ${download_url}"
|
||||
|
||||
# Download the binary
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsSL "$download_url" -o "$temp_file"
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q "$download_url" -O "$temp_file"
|
||||
else
|
||||
print_error "Neither curl nor wget is available. Please install one of them."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create install directory if it doesn't exist
|
||||
mkdir -p "$install_dir"
|
||||
|
||||
# Move binary to install directory
|
||||
mv "$temp_file" "$final_path"
|
||||
|
||||
# Make executable
|
||||
chmod +x "$final_path"
|
||||
|
||||
print_status "Installer downloaded to ${final_path}"
|
||||
}
|
||||
|
||||
# Verify installation
|
||||
verify_installation() {
|
||||
local install_dir="$1"
|
||||
local installer_path="${install_dir}/installer"
|
||||
|
||||
if [ -f "$installer_path" ] && [ -x "$installer_path" ]; then
|
||||
print_status "Installation successful!"
|
||||
return 0
|
||||
else
|
||||
print_error "Installation failed. Binary not found or not executable."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main installation process
|
||||
main() {
|
||||
print_status "Installing latest version of installer..."
|
||||
|
||||
# Get latest version
|
||||
print_status "Fetching latest version from GitHub..."
|
||||
VERSION=$(get_latest_version)
|
||||
print_status "Latest version: v${VERSION}"
|
||||
|
||||
# Set base URL with the fetched version
|
||||
BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}"
|
||||
|
||||
# Detect platform
|
||||
PLATFORM=$(detect_platform)
|
||||
print_status "Detected platform: ${PLATFORM}"
|
||||
|
||||
# Get install directory
|
||||
INSTALL_DIR=$(get_install_dir)
|
||||
print_status "Install directory: ${INSTALL_DIR}"
|
||||
|
||||
# Install installer
|
||||
install_installer "$PLATFORM" "$INSTALL_DIR"
|
||||
|
||||
# Verify installation
|
||||
if verify_installation "$INSTALL_DIR"; then
|
||||
print_status "Installer is ready to use!"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Безплатен план",
|
||||
"billingWarningOverLimit": "Предупреждение: Превишили сте една или повече лимити за използване. Вашите сайтове няма да се свържат, докато не промените абонамента си или не коригирате използването.",
|
||||
"billingUsageLimitsOverview": "Преглед на лимитите за използване",
|
||||
"billingMonitorUsage": "Следете използването спрямо конфигурираните лимити. Ако ви е необходимо увеличаване на лимитите, моля, свържете се с нас на support@fossorial.io.",
|
||||
"billingMonitorUsage": "Следете използването спрямо конфигурираните лимити. Ако ви е необходимо увеличаване на лимитите, моля, свържете се с нас на support@pangolin.net.",
|
||||
"billingDataUsage": "Използване на данни",
|
||||
"billingOnlineTime": "Време на работа на сайта",
|
||||
"billingUsers": "Активни потребители",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Volná úroveň",
|
||||
"billingWarningOverLimit": "Upozornění: Překročili jste jeden nebo více omezení používání. Vaše stránky se nepřipojí dokud nezměníte předplatné nebo neupravíte své používání.",
|
||||
"billingUsageLimitsOverview": "Přehled omezení použití",
|
||||
"billingMonitorUsage": "Sledujte vaše využití pomocí nastavených limitů. Pokud potřebujete zvýšit limity, kontaktujte nás prosím support@fossorial.io.",
|
||||
"billingMonitorUsage": "Sledujte vaše využití pomocí nastavených limitů. Pokud potřebujete zvýšit limity, kontaktujte nás prosím support@pangolin.net.",
|
||||
"billingDataUsage": "Využití dat",
|
||||
"billingOnlineTime": "Stránka online čas",
|
||||
"billingUsers": "Aktivní uživatelé",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Kostenlose Stufe",
|
||||
"billingWarningOverLimit": "Warnung: Sie haben ein oder mehrere Nutzungslimits überschritten. Ihre Webseiten werden nicht verbunden, bis Sie Ihr Abonnement ändern oder Ihren Verbrauch anpassen.",
|
||||
"billingUsageLimitsOverview": "Übersicht über Nutzungsgrenzen",
|
||||
"billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@fossorial.io.",
|
||||
"billingMonitorUsage": "Überwachen Sie Ihren Verbrauch im Vergleich zu konfigurierten Grenzwerten. Wenn Sie eine Erhöhung der Limits benötigen, kontaktieren Sie uns bitte support@pangolin.net.",
|
||||
"billingDataUsage": "Datenverbrauch",
|
||||
"billingOnlineTime": "Online-Zeit der Seite",
|
||||
"billingUsers": "Aktive Benutzer",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -47,9 +47,8 @@
|
||||
"edit": "Edit",
|
||||
"siteConfirmDelete": "Confirm Delete Site",
|
||||
"siteDelete": "Delete Site",
|
||||
"siteMessageRemove": "Once removed, the site will no longer be accessible. All resources and targets associated with the site will also be removed.",
|
||||
"siteMessageConfirm": "To confirm, please type the name of the site below.",
|
||||
"siteQuestionRemove": "Are you sure you want to remove the site {selectedSite} from the organization?",
|
||||
"siteMessageRemove": "Once removed the site will no longer be accessible. All targets associated with the site will also be removed.",
|
||||
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
|
||||
"siteManageSites": "Manage Sites",
|
||||
"siteDescription": "Allow connectivity to your network through secure tunnels",
|
||||
"siteCreate": "Create Site",
|
||||
@@ -154,8 +153,7 @@
|
||||
"protected": "Protected",
|
||||
"notProtected": "Not Protected",
|
||||
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
||||
"resourceMessageConfirm": "To confirm, please type the name of the resource below.",
|
||||
"resourceQuestionRemove": "Are you sure you want to remove the resource {selectedResource} from the organization?",
|
||||
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
||||
"resourceHTTP": "HTTPS Resource",
|
||||
"resourceHTTPDescription": "Proxy requests to your app over HTTPS using a subdomain or base domain.",
|
||||
"resourceRaw": "Raw TCP/UDP Resource",
|
||||
@@ -220,7 +218,7 @@
|
||||
"orgDeleteConfirm": "Confirm Delete Organization",
|
||||
"orgMessageRemove": "This action is irreversible and will delete all associated data.",
|
||||
"orgMessageConfirm": "To confirm, please type the name of the organization below.",
|
||||
"orgQuestionRemove": "Are you sure you want to remove the organization {selectedOrg}?",
|
||||
"orgQuestionRemove": "Are you sure you want to remove the organization?",
|
||||
"orgUpdated": "Organization updated",
|
||||
"orgUpdatedDescription": "The organization has been updated.",
|
||||
"orgErrorUpdate": "Failed to update organization",
|
||||
@@ -287,9 +285,8 @@
|
||||
"apiKeysAdd": "Generate API Key",
|
||||
"apiKeysErrorDelete": "Error deleting API key",
|
||||
"apiKeysErrorDeleteMessage": "Error deleting API key",
|
||||
"apiKeysQuestionRemove": "Are you sure you want to remove the API key {selectedApiKey} from the organization?",
|
||||
"apiKeysQuestionRemove": "Are you sure you want to remove the API key from the organization?",
|
||||
"apiKeysMessageRemove": "Once removed, the API key will no longer be able to be used.",
|
||||
"apiKeysMessageConfirm": "To confirm, please type the name of the API key below.",
|
||||
"apiKeysDeleteConfirm": "Confirm Delete API Key",
|
||||
"apiKeysDelete": "Delete API Key",
|
||||
"apiKeysManage": "Manage API Keys",
|
||||
@@ -305,8 +302,7 @@
|
||||
"userDeleteConfirm": "Confirm Delete User",
|
||||
"userDeleteServer": "Delete User from Server",
|
||||
"userMessageRemove": "The user will be removed from all organizations and be completely removed from the server.",
|
||||
"userMessageConfirm": "To confirm, please type the name of the user below.",
|
||||
"userQuestionRemove": "Are you sure you want to permanently delete {selectedUser} from the server?",
|
||||
"userQuestionRemove": "Are you sure you want to permanently delete user from the server?",
|
||||
"licenseKey": "License Key",
|
||||
"valid": "Valid",
|
||||
"numberOfSites": "Number of Sites",
|
||||
@@ -339,7 +335,7 @@
|
||||
"fossorialLicense": "View Fossorial Commercial License & Subscription Terms",
|
||||
"licenseMessageRemove": "This will remove the license key and all associated permissions granted by it.",
|
||||
"licenseMessageConfirm": "To confirm, please type the license key below.",
|
||||
"licenseQuestionRemove": "Are you sure you want to delete the license key {selectedKey} ?",
|
||||
"licenseQuestionRemove": "Are you sure you want to delete the license key ?",
|
||||
"licenseKeyDelete": "Delete License Key",
|
||||
"licenseKeyDeleteConfirm": "Confirm Delete License Key",
|
||||
"licenseTitle": "Manage License Status",
|
||||
@@ -372,7 +368,7 @@
|
||||
"inviteRemoveErrorDescription": "An error occurred while removing the invitation.",
|
||||
"inviteRemoved": "Invitation removed",
|
||||
"inviteRemovedDescription": "The invitation for {email} has been removed.",
|
||||
"inviteQuestionRemove": "Are you sure you want to remove the invitation {email}?",
|
||||
"inviteQuestionRemove": "Are you sure you want to remove the invitation?",
|
||||
"inviteMessageRemove": "Once removed, this invitation will no longer be valid. You can always re-invite the user later.",
|
||||
"inviteMessageConfirm": "To confirm, please type the email address of the invitation below.",
|
||||
"inviteQuestionRegenerate": "Are you sure you want to regenerate the invitation for {email}? This will revoke the previous invitation.",
|
||||
@@ -398,9 +394,8 @@
|
||||
"userErrorOrgRemoveDescription": "An error occurred while removing the user.",
|
||||
"userOrgRemoved": "User removed",
|
||||
"userOrgRemovedDescription": "The user {email} has been removed from the organization.",
|
||||
"userQuestionOrgRemove": "Are you sure you want to remove {email} from the organization?",
|
||||
"userQuestionOrgRemove": "Are you sure you want to remove this user from the organization?",
|
||||
"userMessageOrgRemove": "Once removed, this user will no longer have access to the organization. You can always re-invite them later, but they will need to accept the invitation again.",
|
||||
"userMessageOrgConfirm": "To confirm, please type the name of the of the user below.",
|
||||
"userRemoveOrgConfirm": "Confirm Remove User",
|
||||
"userRemoveOrg": "Remove User from Organization",
|
||||
"users": "Users",
|
||||
@@ -742,7 +737,7 @@
|
||||
"idpManageDescription": "View and manage identity providers in the system",
|
||||
"idpDeletedDescription": "Identity provider deleted successfully",
|
||||
"idpOidc": "OAuth2/OIDC",
|
||||
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider {name}?",
|
||||
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?",
|
||||
"idpMessageRemove": "This will remove the identity provider and all associated configurations. Users who authenticate through this provider will no longer be able to log in.",
|
||||
"idpMessageConfirm": "To confirm, please type the name of the identity provider below.",
|
||||
"idpConfirmDelete": "Confirm Delete Identity Provider",
|
||||
@@ -1211,9 +1206,8 @@
|
||||
"domainCreate": "Create Domain",
|
||||
"domainCreatedDescription": "Domain created successfully",
|
||||
"domainDeletedDescription": "Domain deleted successfully",
|
||||
"domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?",
|
||||
"domainQuestionRemove": "Are you sure you want to remove the domain from your account?",
|
||||
"domainMessageRemove": "Once removed, the domain will no longer be associated with your account.",
|
||||
"domainMessageConfirm": "To confirm, please type the domain name below.",
|
||||
"domainConfirmDelete": "Confirm Delete Domain",
|
||||
"domainDelete": "Delete Domain",
|
||||
"domain": "Domain",
|
||||
@@ -1280,7 +1274,7 @@
|
||||
"billingFreeTier": "Free Tier",
|
||||
"billingWarningOverLimit": "Warning: You have exceeded one or more usage limits. Your sites will not connect until you modify your subscription or adjust your usage.",
|
||||
"billingUsageLimitsOverview": "Usage Limits Overview",
|
||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
||||
"billingDataUsage": "Data Usage",
|
||||
"billingOnlineTime": "Site Online Time",
|
||||
"billingUsers": "Active Users",
|
||||
@@ -1563,9 +1557,8 @@
|
||||
"searchRemoteExitNodes": "Search nodes...",
|
||||
"remoteExitNodeAdd": "Add Node",
|
||||
"remoteExitNodeErrorDelete": "Error deleting node",
|
||||
"remoteExitNodeQuestionRemove": "Are you sure you want to remove the node {selectedNode} from the organization?",
|
||||
"remoteExitNodeQuestionRemove": "Are you sure you want to remove the node from the organization?",
|
||||
"remoteExitNodeMessageRemove": "Once removed, the node will no longer be accessible.",
|
||||
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
||||
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
||||
"remoteExitNodeDelete": "Delete Node",
|
||||
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||
@@ -1819,7 +1812,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1839,7 +1832,7 @@
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
"complianceConfirmation": "I confirm that the information I provided is accurate and that I am in compliance with the Fossorial Commercial License. Reporting inaccurate information or misidentifying use of the product is a violation of the license and may result in your key getting revoked."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
@@ -1893,5 +1886,10 @@
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
"pathRewriteStripLabel": "strip"
|
||||
"pathRewriteStripLabel": "strip",
|
||||
"sidebarEnableEnterpriseLicense": "Enable Enterprise License",
|
||||
"cannotbeUndone": "This can not be undone.",
|
||||
"toConfirm": "to confirm",
|
||||
"deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?",
|
||||
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site."
|
||||
}
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Nivel Gratis",
|
||||
"billingWarningOverLimit": "Advertencia: Has excedido uno o más límites de uso. Tus sitios no se conectarán hasta que modifiques tu suscripción o ajustes tu uso.",
|
||||
"billingUsageLimitsOverview": "Descripción general de los límites de uso",
|
||||
"billingMonitorUsage": "Monitorea tu uso comparado con los límites configurados. Si necesitas que aumenten los límites, contáctanos a soporte@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitorea tu uso comparado con los límites configurados. Si necesitas que aumenten los límites, contáctanos a soporte@pangolin.net.",
|
||||
"billingDataUsage": "Uso de datos",
|
||||
"billingOnlineTime": "Tiempo en línea del sitio",
|
||||
"billingUsers": "Usuarios activos",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -6,45 +6,45 @@
|
||||
"setupOrgName": "Nom de l'organisation",
|
||||
"orgDisplayName": "Ceci est le nom d'affichage de votre organisation.",
|
||||
"orgId": "ID de l'organisation",
|
||||
"setupIdentifierMessage": "Ceci est l'identifiant unique pour votre organisation. Il est séparé du nom affiché.",
|
||||
"setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.",
|
||||
"setupIdentifierMessage": "Ceci est l'identifiant de votre organisation. Il est différent du nom affiché.",
|
||||
"setupErrorIdentifier": "Cet identifiant est déjà pris. Veuillez en choisir un autre.",
|
||||
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
|
||||
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
|
||||
"welcome": "Bienvenue à Pangolin",
|
||||
"welcome": "Bienvenue sur Pangolin",
|
||||
"welcomeTo": "Bienvenue chez",
|
||||
"componentsCreateOrg": "Créer une organisation",
|
||||
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.",
|
||||
"componentsMember": "Vous {count, plural, =0 {n'} other {}}êtes membre {count, plural, =0 {d'aucune organisation} one {d'une organisation} other {de # organisations}}.",
|
||||
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
||||
"dismiss": "Refuser",
|
||||
"componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
|
||||
"componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!",
|
||||
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.",
|
||||
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour cet utilisateur.",
|
||||
"inviteLoginUser": "Assurez-vous que vous êtes bien connecté en tant qu'utilisateur correct.",
|
||||
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour un utilisateur qui existe.",
|
||||
"inviteCreateUser": "Veuillez d'abord créer un compte.",
|
||||
"goHome": "Retour à la maison",
|
||||
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.",
|
||||
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder ne soit pas pour cet utilisateur.",
|
||||
"inviteLoginUser": "Assurez-vous d'etre bien connecté au bon compte.",
|
||||
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder ne soit pas pour un utilisateur qui existe.",
|
||||
"inviteCreateUser": "Vous n'avez aucun compte, veuillez en créer un.",
|
||||
"goHome": "Retour à l'accueil",
|
||||
"inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent",
|
||||
"createAnAccount": "Créer un compte",
|
||||
"inviteNotAccepted": "Invitation non acceptée",
|
||||
"authCreateAccount": "Créez un compte pour commencer",
|
||||
"authNoAccount": "Vous n'avez pas de compte ?",
|
||||
"email": "Courriel",
|
||||
"email": "Adresse email",
|
||||
"password": "Mot de passe",
|
||||
"confirmPassword": "Confirmer le mot de passe",
|
||||
"createAccount": "Créer un compte",
|
||||
"viewSettings": "Afficher les paramètres",
|
||||
"delete": "Supprimez",
|
||||
"delete": "Supprimer",
|
||||
"name": "Nom",
|
||||
"online": "En ligne",
|
||||
"offline": "Hors ligne",
|
||||
"site": "Site",
|
||||
"dataIn": "Données dans",
|
||||
"dataOut": "Données épuisées",
|
||||
"dataIn": "Données reçues",
|
||||
"dataOut": "Données émises",
|
||||
"connectionType": "Type de connexion",
|
||||
"tunnelType": "Type de tunnel",
|
||||
"local": "Locale",
|
||||
"edit": "Editer",
|
||||
"edit": "Modifier",
|
||||
"siteConfirmDelete": "Confirmer la suppression du site",
|
||||
"siteDelete": "Supprimer le site",
|
||||
"siteMessageRemove": "Une fois supprimé, le site ne sera plus accessible. Toutes les ressources et cibles associées au site seront également supprimées.",
|
||||
@@ -64,11 +64,11 @@
|
||||
"siteLearnNewt": "Apprenez à installer Newt sur votre système",
|
||||
"siteSeeConfigOnce": "Vous ne pourrez voir la configuration qu'une seule fois.",
|
||||
"siteLoadWGConfig": "Chargement de la configuration WireGuard...",
|
||||
"siteDocker": "Développer les détails du déploiement Docker",
|
||||
"siteDocker": "Afficher les détails du déploiement Docker",
|
||||
"toggle": "Activer/désactiver",
|
||||
"dockerCompose": "Composition Docker",
|
||||
"dockerRun": "Exécution Docker",
|
||||
"siteLearnLocal": "Les sites locaux ne tunnel, en savoir plus",
|
||||
"dockerCompose": "Docker Compose",
|
||||
"dockerRun": "Docker Run",
|
||||
"siteLearnLocal": "Les sites locaux ne permettent pas d'utiliser les tunnel, en savoir plus",
|
||||
"siteConfirmCopy": "J'ai copié la configuration",
|
||||
"searchSitesProgress": "Rechercher des sites...",
|
||||
"siteAdd": "Ajouter un site",
|
||||
@@ -79,9 +79,9 @@
|
||||
"operatingSystem": "Système d'exploitation",
|
||||
"commands": "Commandes",
|
||||
"recommended": "Recommandé",
|
||||
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Il utilise WireGuard sous le capot et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.",
|
||||
"siteRunsInDocker": "Exécute dans Docker",
|
||||
"siteRunsInShell": "Exécute en shell sur macOS, Linux et Windows",
|
||||
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Newt se base sur WireGuard et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.",
|
||||
"siteRunsInDocker": "S'exécute dans Docker",
|
||||
"siteRunsInShell": "S'exécute en shell sur macOS, Linux et Windows",
|
||||
"siteErrorDelete": "Erreur lors de la suppression du site",
|
||||
"siteErrorUpdate": "Impossible de mettre à jour le site",
|
||||
"siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du site.",
|
||||
@@ -89,18 +89,18 @@
|
||||
"siteUpdatedDescription": "Le site a été mis à jour.",
|
||||
"siteGeneralDescription": "Configurer les paramètres généraux de ce site",
|
||||
"siteSettingDescription": "Configurer les paramètres de votre site",
|
||||
"siteSetting": "Réglages {siteName}",
|
||||
"siteSetting": "Réglages de {siteName}",
|
||||
"siteNewtTunnel": "Tunnel Newt (Recommandé)",
|
||||
"siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.",
|
||||
"siteWg": "WireGuard basique",
|
||||
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
|
||||
"siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
|
||||
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
|
||||
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
|
||||
"siteLocalDescriptionSaas": "Ressources locales seulement. Pas de tunneling. Seulement disponible sur les noeuds distants",
|
||||
"siteSeeAll": "Voir tous les sites",
|
||||
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
|
||||
"siteNewtCredentials": "Identifiants Newt",
|
||||
"siteNewtCredentialsDescription": "C'est ainsi que Newt s'authentifiera avec le serveur",
|
||||
"siteNewtCredentialsDescription": "C'est comme cela que Newt s'authentifiera avec le serveur",
|
||||
"siteCredentialsSave": "Enregistrez vos identifiants",
|
||||
"siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de le copier dans un endroit sécurisé.",
|
||||
"siteInfo": "Informations sur le site",
|
||||
@@ -112,7 +112,7 @@
|
||||
"shareErrorDelete": "Impossible de supprimer le lien",
|
||||
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
|
||||
"shareDeleted": "Lien supprimé",
|
||||
"shareDeletedDescription": "Le lien a été supprimé",
|
||||
"shareDeletedDescription": "Le lien de partage a été supprimé",
|
||||
"shareTokenDescription": "Votre jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
|
||||
"accessToken": "Jeton d'accès",
|
||||
"usageExamples": "Exemples d'utilisation",
|
||||
@@ -134,7 +134,7 @@
|
||||
"shareExpireDescription": "Le temps d'expiration est combien de temps le lien sera utilisable et fournira un accès à la ressource. Après cette période, le lien ne fonctionnera plus et les utilisateurs qui ont utilisé ce lien perdront l'accès à la ressource.",
|
||||
"shareSeeOnce": "Vous ne pourrez voir ce lien. Assurez-vous de le copier.",
|
||||
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec soin.",
|
||||
"shareTokenUsage": "Voir Utilisation du jeton d'accès",
|
||||
"shareTokenUsage": "Voir l'utilisation du jeton d'accès",
|
||||
"createLink": "Créer un lien",
|
||||
"resourcesNotFound": "Aucune ressource trouvée",
|
||||
"resourceSearch": "Rechercher des ressources",
|
||||
@@ -1234,7 +1234,7 @@
|
||||
"billing": "Facturation",
|
||||
"orgBillingDescription": "Gérez vos informations de facturation et vos abonnements",
|
||||
"github": "GitHub",
|
||||
"pangolinHosted": "Pangolin Hébergement",
|
||||
"pangolinHosted": "Hebergé par Pangolin",
|
||||
"fossorial": "Fossorial",
|
||||
"completeAccountSetup": "Complétez la configuration du compte",
|
||||
"completeAccountSetupDescription": "Définissez votre mot de passe pour commencer",
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Niveau gratuit",
|
||||
"billingWarningOverLimit": "Attention : Vous avez dépassé une ou plusieurs limites d'utilisation. Vos sites ne se connecteront pas tant que vous n'avez pas modifié votre abonnement ou ajusté votre utilisation.",
|
||||
"billingUsageLimitsOverview": "Vue d'ensemble des limites d'utilisation",
|
||||
"billingMonitorUsage": "Surveillez votre consommation par rapport aux limites configurées. Si vous avez besoin d'une augmentation des limites, veuillez nous contacter à support@fossorial.io.",
|
||||
"billingMonitorUsage": "Surveillez votre consommation par rapport aux limites configurées. Si vous avez besoin d'une augmentation des limites, veuillez nous contacter à support@pangolin.net.",
|
||||
"billingDataUsage": "Utilisation des données",
|
||||
"billingOnlineTime": "Temps en ligne du site",
|
||||
"billingUsers": "Utilisateurs actifs",
|
||||
@@ -1316,7 +1316,7 @@
|
||||
"billingRemoteExitNodesInfo": "Vous êtes facturé pour chaque nœud géré dans votre organisation. La facturation est calculée quotidiennement en fonction du nombre de nœuds gérés actifs dans votre organisation.",
|
||||
"domainNotFound": "Domaine introuvable",
|
||||
"domainNotFoundDescription": "Cette ressource est désactivée car le domaine n'existe plus dans notre système. Veuillez définir un nouveau domaine pour cette ressource.",
|
||||
"failed": "Échec",
|
||||
"failed": "Erreur",
|
||||
"createNewOrgDescription": "Créer une nouvelle organisation",
|
||||
"organization": "Organisation",
|
||||
"port": "Port",
|
||||
@@ -1370,7 +1370,7 @@
|
||||
"createDomainARecords": "Enregistrements A",
|
||||
"createDomainRecordNumber": "Enregistrement {number}",
|
||||
"createDomainTxtRecords": "Enregistrements TXT",
|
||||
"createDomainSaveTheseRecords": "Enregistrez ces enregistrements",
|
||||
"createDomainSaveTheseRecords": "Sauvegardez ces enregistrements",
|
||||
"createDomainSaveTheseRecordsDescription": "Assurez-vous de sauvegarder ces enregistrements DNS car vous ne les reverrez pas.",
|
||||
"createDomainDnsPropagation": "Propagation DNS",
|
||||
"createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.",
|
||||
@@ -1445,7 +1445,7 @@
|
||||
"IntervalSeconds": "Intervalle sain",
|
||||
"timeoutSeconds": "Délai",
|
||||
"timeIsInSeconds": "Le temps est exprimé en secondes",
|
||||
"retryAttempts": "Tentatives de réessai",
|
||||
"retryAttempts": "Tentatives",
|
||||
"expectedResponseCodes": "Codes de réponse attendus",
|
||||
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
|
||||
"customHeaders": "En-têtes personnalisés",
|
||||
@@ -1760,9 +1760,9 @@
|
||||
"enterpriseEdition": "Enterprise Edition",
|
||||
"unlicensed": "Unlicensed",
|
||||
"beta": "Beta",
|
||||
"manageClients": "Manage Clients",
|
||||
"manageClients": "Gérer les clients",
|
||||
"manageClientsDescription": "Clients are devices that can connect to your sites",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
"licenseTableValidUntil": "Valide jusqu'au",
|
||||
"saasLicenseKeysSettingsTitle": "Enterprise Licenses",
|
||||
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
|
||||
"sidebarEnterpriseLicenses": "Licenses",
|
||||
@@ -1771,8 +1771,8 @@
|
||||
"validation": {
|
||||
"emailRequired": "Please enter a valid email address",
|
||||
"useCaseTypeRequired": "Please select a use case type",
|
||||
"firstNameRequired": "First name is required",
|
||||
"lastNameRequired": "Last name is required",
|
||||
"firstNameRequired": "Le prénom est requis",
|
||||
"lastNameRequired": "Le nom est requis",
|
||||
"primaryUseRequired": "Please describe your primary use",
|
||||
"jobTitleRequiredBusiness": "Job title is required for business use",
|
||||
"industryRequiredBusiness": "Industry is required for business use",
|
||||
@@ -1786,7 +1786,7 @@
|
||||
},
|
||||
"useCaseOptions": {
|
||||
"personal": {
|
||||
"title": "Personal Use",
|
||||
"title": "Utilisation personelle",
|
||||
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
|
||||
},
|
||||
"business": {
|
||||
@@ -1819,32 +1819,32 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"useCaseQuestion": "Are you using Pangolin for personal or business use?",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"jobTitle": "Job Title",
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
"jobTitle": "profession",
|
||||
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
|
||||
"industryQuestion": "What is your industry?",
|
||||
"prospectiveUsersQuestion": "How many prospective users do you expect to have?",
|
||||
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
|
||||
"companyName": "Company name",
|
||||
"countryOfResidence": "Country of residence",
|
||||
"stateProvinceRegion": "State / Province / Region",
|
||||
"postalZipCode": "Postal / ZIP Code",
|
||||
"companyWebsite": "Company website",
|
||||
"companyPhoneNumber": "Company phone number",
|
||||
"country": "Country",
|
||||
"phoneNumberOptional": "Phone number (optional)",
|
||||
"companyName": "Entreprise",
|
||||
"countryOfResidence": "Pays de résidence",
|
||||
"stateProvinceRegion": "État / Province / Région",
|
||||
"postalZipCode": "Code postal",
|
||||
"companyWebsite": "Site de l'entreprise",
|
||||
"companyPhoneNumber": "Numéro de téléphone professionnel",
|
||||
"country": "Pays",
|
||||
"phoneNumberOptional": "Numéro de téléphone (optionnel)",
|
||||
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
|
||||
},
|
||||
"buttons": {
|
||||
"close": "Close",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"close": "Fermer",
|
||||
"previous": "Précédent",
|
||||
"next": "Suivant",
|
||||
"generateLicenseKey": "Generate License Key"
|
||||
},
|
||||
"toasts": {
|
||||
@@ -1860,16 +1860,16 @@
|
||||
},
|
||||
"priority": "Priorité",
|
||||
"priorityDescription": "Les routes de haute priorité sont évaluées en premier. La priorité = 100 signifie l'ordre automatique (décision du système). Utilisez un autre nombre pour imposer la priorité manuelle.",
|
||||
"instanceName": "Instance Name",
|
||||
"instanceName": "Nom de l'instance",
|
||||
"pathMatchModalTitle": "Configure Path Matching",
|
||||
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
|
||||
"pathMatchType": "Match Type",
|
||||
"pathMatchPrefix": "Prefix",
|
||||
"pathMatchPrefix": "Préfix",
|
||||
"pathMatchExact": "Exact",
|
||||
"pathMatchRegex": "Regex",
|
||||
"pathMatchValue": "Path Value",
|
||||
"clear": "Clear",
|
||||
"saveChanges": "Save Changes",
|
||||
"saveChanges": "Sauvegarder",
|
||||
"pathMatchRegexPlaceholder": "^/api/.*",
|
||||
"pathMatchDefaultPlaceholder": "/path",
|
||||
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
|
||||
@@ -1889,7 +1889,7 @@
|
||||
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
|
||||
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
|
||||
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
|
||||
"pathRewritePrefix": "Prefix",
|
||||
"pathRewritePrefix": "Préfix",
|
||||
"pathRewriteExact": "Exact",
|
||||
"pathRewriteRegex": "Regex",
|
||||
"pathRewriteStrip": "Strip",
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Piano Gratuito",
|
||||
"billingWarningOverLimit": "Avviso: Hai superato uno o più limiti di utilizzo. I tuoi siti non si connetteranno finché non modifichi il tuo abbonamento o non adegui il tuo utilizzo.",
|
||||
"billingUsageLimitsOverview": "Panoramica dei Limiti di Utilizzo",
|
||||
"billingMonitorUsage": "Monitora il tuo utilizzo rispetto ai limiti configurati. Se hai bisogno di aumentare i limiti, contattaci all'indirizzo support@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitora il tuo utilizzo rispetto ai limiti configurati. Se hai bisogno di aumentare i limiti, contattaci all'indirizzo support@pangolin.net.",
|
||||
"billingDataUsage": "Utilizzo dei Dati",
|
||||
"billingOnlineTime": "Tempo Online del Sito",
|
||||
"billingUsers": "Utenti Attivi",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "무료 티어",
|
||||
"billingWarningOverLimit": "경고: 하나 이상의 사용 한도를 초과했습니다. 구독을 수정하거나 사용량을 조정하기 전까지 사이트는 연결되지 않습니다.",
|
||||
"billingUsageLimitsOverview": "사용 한도 개요",
|
||||
"billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@fossorial.io로 연락하십시오.",
|
||||
"billingMonitorUsage": "설정된 한도에 대한 사용량을 모니터링합니다. 한도를 늘려야 하는 경우 support@pangolin.net로 연락하십시오.",
|
||||
"billingDataUsage": "데이터 사용량",
|
||||
"billingOnlineTime": "사이트 온라인 시간",
|
||||
"billingUsers": "활성 사용자",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Gratis nivå",
|
||||
"billingWarningOverLimit": "Advarsel: Du har overskredet en eller flere bruksgrenser. Nettstedene dine vil ikke koble til før du endrer abonnementet ditt eller justerer bruken.",
|
||||
"billingUsageLimitsOverview": "Oversikt over bruksgrenser",
|
||||
"billingMonitorUsage": "Overvåk bruken din i forhold til konfigurerte grenser. Hvis du trenger økte grenser, vennligst kontakt support@fossorial.io.",
|
||||
"billingMonitorUsage": "Overvåk bruken din i forhold til konfigurerte grenser. Hvis du trenger økte grenser, vennligst kontakt support@pangolin.net.",
|
||||
"billingDataUsage": "Databruk",
|
||||
"billingOnlineTime": "Online tid for nettsteder",
|
||||
"billingUsers": "Aktive brukere",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Gratis Niveau",
|
||||
"billingWarningOverLimit": "Waarschuwing: U hebt een of meer gebruikslimieten overschreden. Uw sites maken geen verbinding totdat u uw abonnement aanpast of uw gebruik aanpast.",
|
||||
"billingUsageLimitsOverview": "Overzicht gebruikslimieten",
|
||||
"billingMonitorUsage": "Houd uw gebruik in de gaten ten opzichte van de ingestelde limieten. Als u verhoogde limieten nodig heeft, neem dan contact met ons op support@fossorial.io.",
|
||||
"billingMonitorUsage": "Houd uw gebruik in de gaten ten opzichte van de ingestelde limieten. Als u verhoogde limieten nodig heeft, neem dan contact met ons op support@pangolin.net.",
|
||||
"billingDataUsage": "Gegevensgebruik",
|
||||
"billingOnlineTime": "Site Online Tijd",
|
||||
"billingUsers": "Actieve Gebruikers",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Darmowy pakiet",
|
||||
"billingWarningOverLimit": "Ostrzeżenie: Przekroczyłeś jeden lub więcej limitów użytkowania. Twoje witryny nie połączą się, dopóki nie zmienisz subskrypcji lub nie dostosujesz użytkowania.",
|
||||
"billingUsageLimitsOverview": "Przegląd Limitów Użytkowania",
|
||||
"billingMonitorUsage": "Monitoruj swoje wykorzystanie w porównaniu do skonfigurowanych limitów. Jeśli potrzebujesz zwiększenia limitów, skontaktuj się z nami pod adresem support@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitoruj swoje wykorzystanie w porównaniu do skonfigurowanych limitów. Jeśli potrzebujesz zwiększenia limitów, skontaktuj się z nami pod adresem support@pangolin.net.",
|
||||
"billingDataUsage": "Użycie danych",
|
||||
"billingOnlineTime": "Czas Online Strony",
|
||||
"billingUsers": "Aktywni użytkownicy",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Plano Gratuito",
|
||||
"billingWarningOverLimit": "Aviso: Você ultrapassou um ou mais limites de uso. Seus sites não se conectarão até você modificar sua assinatura ou ajustar seu uso.",
|
||||
"billingUsageLimitsOverview": "Visão Geral dos Limites de Uso",
|
||||
"billingMonitorUsage": "Monitore seu uso em relação aos limites configurados. Se precisar aumentar esses limites, entre em contato conosco support@fossorial.io.",
|
||||
"billingMonitorUsage": "Monitore seu uso em relação aos limites configurados. Se precisar aumentar esses limites, entre em contato conosco support@pangolin.net.",
|
||||
"billingDataUsage": "Uso de Dados",
|
||||
"billingOnlineTime": "Tempo Online do Site",
|
||||
"billingUsers": "Usuários Ativos",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Бесплатный уровень",
|
||||
"billingWarningOverLimit": "Предупреждение: Вы превысили одну или несколько границ использования. Ваши сайты не подключатся, пока вы не измените подписку или не скорректируете использование.",
|
||||
"billingUsageLimitsOverview": "Обзор лимитов использования",
|
||||
"billingMonitorUsage": "Контролируйте использование в соответствии с установленными лимитами. Если вам требуется увеличение лимитов, пожалуйста, свяжитесь с нами support@fossorial.io.",
|
||||
"billingMonitorUsage": "Контролируйте использование в соответствии с установленными лимитами. Если вам требуется увеличение лимитов, пожалуйста, свяжитесь с нами support@pangolin.net.",
|
||||
"billingDataUsage": "Использование данных",
|
||||
"billingOnlineTime": "Время работы сайта",
|
||||
"billingUsers": "Активные пользователи",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "Ücretsiz Dilim",
|
||||
"billingWarningOverLimit": "Uyarı: Bir veya daha fazla kullanım limitini aştınız. Aboneliğinizi değiştirmediğiniz veya kullanımı ayarlamadığınız sürece siteleriniz bağlanmayacaktır.",
|
||||
"billingUsageLimitsOverview": "Kullanım Limitleri Genel Görünümü",
|
||||
"billingMonitorUsage": "Kullanımınızı yapılandırılmış limitlerle karşılaştırın. Limitlerin artırılmasına ihtiyacınız varsa, lütfen support@fossorial.io adresinden bizimle iletişime geçin.",
|
||||
"billingMonitorUsage": "Kullanımınızı yapılandırılmış limitlerle karşılaştırın. Limitlerin artırılmasına ihtiyacınız varsa, lütfen support@pangolin.net adresinden bizimle iletişime geçin.",
|
||||
"billingDataUsage": "Veri Kullanımı",
|
||||
"billingOnlineTime": "Site Çevrimiçi Süresi",
|
||||
"billingUsers": "Aktif Kullanıcılar",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"billingFreeTier": "免费层",
|
||||
"billingWarningOverLimit": "警告:您已超出一个或多个使用限制。在您修改订阅或调整使用情况之前,您的站点将无法连接。",
|
||||
"billingUsageLimitsOverview": "使用限制概览",
|
||||
"billingMonitorUsage": "监控您的使用情况以对比已配置的限制。如需提高限制请联系我们 support@fossorial.io。",
|
||||
"billingMonitorUsage": "监控您的使用情况以对比已配置的限制。如需提高限制请联系我们 support@pangolin.net。",
|
||||
"billingDataUsage": "数据使用情况",
|
||||
"billingOnlineTime": "站点在线时间",
|
||||
"billingUsers": "活跃用户",
|
||||
@@ -1819,7 +1819,7 @@
|
||||
},
|
||||
"trialPeriodInformation": {
|
||||
"title": "Trial Period Information",
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
|
||||
"description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@pangolin.net."
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
|
||||
4493
package-lock.json
generated
4493
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@@ -56,7 +56,7 @@
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-email/components": "0.5.6",
|
||||
"@react-email/components": "0.5.7",
|
||||
"@react-email/render": "^1.3.2",
|
||||
"@react-email/tailwind": "1.2.2",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
@@ -77,7 +77,7 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"drizzle-orm": "0.44.6",
|
||||
"eslint": "9.37.0",
|
||||
"eslint-config-next": "15.5.4",
|
||||
"eslint-config-next": "15.5.6",
|
||||
"express": "5.1.0",
|
||||
"express-rate-limit": "8.1.0",
|
||||
"glob": "11.0.3",
|
||||
@@ -92,7 +92,7 @@
|
||||
"lucide-react": "^0.545.0",
|
||||
"maxmind": "5.0.0",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.4",
|
||||
"next": "15.5.6",
|
||||
"next-intl": "^4.3.12",
|
||||
"next-themes": "0.4.6",
|
||||
"node-cache": "5.1.2",
|
||||
@@ -143,13 +143,13 @@
|
||||
"@types/nodemailer": "7.0.2",
|
||||
"@types/pg": "8.15.5",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.1",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/ws": "8.18.1",
|
||||
"@types/yargs": "17.0.33",
|
||||
"drizzle-kit": "0.31.5",
|
||||
"esbuild": "0.25.10",
|
||||
"esbuild": "0.25.11",
|
||||
"esbuild-node-externals": "1.18.0",
|
||||
"postcss": "^8",
|
||||
"react-email": "4.3.0",
|
||||
|
||||
@@ -126,7 +126,7 @@ export const targets = pgTable("targets", {
|
||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||
priority: integer("priority").notNull().default(100)
|
||||
priority: integer("priority").default(100)
|
||||
});
|
||||
|
||||
export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
|
||||
@@ -138,7 +138,7 @@ export const targets = sqliteTable("targets", {
|
||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
|
||||
priority: integer("priority").notNull().default(100)
|
||||
priority: integer("priority").default(100)
|
||||
});
|
||||
|
||||
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
|
||||
|
||||
@@ -88,7 +88,7 @@ export const WelcomeQuickStart = ({
|
||||
To learn how to use Newt, including more
|
||||
installation methods, visit the{" "}
|
||||
<a
|
||||
href="https://docs.digpangolin.com/manage/sites/install-site"
|
||||
href="https://docs.pangolin.net/manage/sites/install-site"
|
||||
className="underline"
|
||||
>
|
||||
docs
|
||||
|
||||
@@ -89,7 +89,7 @@ export function EmailFooter({ children }: { children: React.ReactNode }) {
|
||||
<p className="text-xs text-gray-400 mt-4">
|
||||
For any questions or support, please contact us at:
|
||||
<br />
|
||||
support@fossorial.io
|
||||
support@pangolin.net
|
||||
</p>
|
||||
<p className="text-xs text-gray-300 text-center mt-4">
|
||||
© {new Date().getFullYear()} Fossorial, Inc. All
|
||||
|
||||
@@ -19,11 +19,16 @@ import { setHostMeta } from "@server/lib/hostMeta";
|
||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
||||
import { initCleanup } from "#dynamic/cleanup";
|
||||
import license from "#dynamic/license/license";
|
||||
|
||||
async function startServers() {
|
||||
await setHostMeta();
|
||||
|
||||
await config.initServer();
|
||||
|
||||
license.setServerSecret(config.getRawConfig().server.secret!);
|
||||
await license.check();
|
||||
|
||||
await runSetupFunctions();
|
||||
|
||||
initTelemetryClient();
|
||||
|
||||
@@ -612,7 +612,8 @@ export class UsageService {
|
||||
|
||||
public async getUsage(
|
||||
orgId: string,
|
||||
featureId: FeatureId
|
||||
featureId: FeatureId,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<Usage | null> {
|
||||
if (noop()) {
|
||||
return null;
|
||||
@@ -621,7 +622,7 @@ export class UsageService {
|
||||
const usageId = `${orgId}-${featureId}`;
|
||||
|
||||
try {
|
||||
const [result] = await db
|
||||
const [result] = await trx
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -635,7 +636,7 @@ export class UsageService {
|
||||
const meterId = getFeatureMeterId(featureId);
|
||||
|
||||
try {
|
||||
const [newUsage] = await db
|
||||
const [newUsage] = await trx
|
||||
.insert(usage)
|
||||
.values({
|
||||
usageId,
|
||||
@@ -652,7 +653,7 @@ export class UsageService {
|
||||
return newUsage;
|
||||
} else {
|
||||
// Record was created by another process, fetch it
|
||||
const [existingUsage] = await db
|
||||
const [existingUsage] = await trx
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -665,7 +666,7 @@ export class UsageService {
|
||||
`Insert failed for ${orgId}/${featureId}, attempting to fetch existing record:`,
|
||||
insertError
|
||||
);
|
||||
const [existingUsage] = await db
|
||||
const [existingUsage] = await trx
|
||||
.select()
|
||||
.from(usage)
|
||||
.where(eq(usage.usageId, usageId))
|
||||
@@ -812,7 +813,8 @@ export class UsageService {
|
||||
orgId: string,
|
||||
kickSites = false,
|
||||
featureId?: FeatureId,
|
||||
usage?: Usage
|
||||
usage?: Usage,
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<boolean> {
|
||||
if (noop()) {
|
||||
return false;
|
||||
@@ -825,7 +827,7 @@ export class UsageService {
|
||||
let orgLimits: Limit[] = [];
|
||||
if (featureId) {
|
||||
// Get all limits set for this organization
|
||||
orgLimits = await db
|
||||
orgLimits = await trx
|
||||
.select()
|
||||
.from(limits)
|
||||
.where(
|
||||
@@ -836,7 +838,7 @@ export class UsageService {
|
||||
);
|
||||
} else {
|
||||
// Get all limits set for this organization
|
||||
orgLimits = await db
|
||||
orgLimits = await trx
|
||||
.select()
|
||||
.from(limits)
|
||||
.where(eq(limits.orgId, orgId));
|
||||
@@ -855,7 +857,8 @@ export class UsageService {
|
||||
} else {
|
||||
currentUsage = await this.getUsage(
|
||||
orgId,
|
||||
limit.featureId as FeatureId
|
||||
limit.featureId as FeatureId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
@@ -890,7 +893,7 @@ export class UsageService {
|
||||
);
|
||||
|
||||
// Get all sites for this organization
|
||||
const orgSites = await db
|
||||
const orgSites = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.orgId, orgId));
|
||||
@@ -902,7 +905,7 @@ export class UsageService {
|
||||
// Send termination messages to newt sites
|
||||
for (const site of orgSites) {
|
||||
if (site.type === "newt") {
|
||||
const [newt] = await db
|
||||
const [newt] = await trx
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
@@ -917,7 +920,7 @@ export class UsageService {
|
||||
};
|
||||
|
||||
// Don't await to prevent blocking
|
||||
sendToClient(newt.newtId, payload).catch(
|
||||
await sendToClient(newt.newtId, payload).catch(
|
||||
(error: any) => {
|
||||
logger.error(
|
||||
`Failed to send termination message to newt ${newt.newtId}:`,
|
||||
|
||||
@@ -139,8 +139,8 @@ export async function applyBlueprint(
|
||||
// password: "sadfasdfadsf",
|
||||
// "sso-enabled": true,
|
||||
// "sso-roles": ["Member"],
|
||||
// "sso-users": ["owen@fossorial.io"],
|
||||
// "whitelist-users": ["owen@fossorial.io"]
|
||||
// "sso-users": ["owen@pangolin.net"],
|
||||
// "whitelist-users": ["owen@pangolin.net"]
|
||||
// },
|
||||
// targets: [
|
||||
// {
|
||||
|
||||
@@ -87,8 +87,8 @@ export function convertValue(value: string): any {
|
||||
// "resources.resource-nice-id.auth.password": "sadfasdfadsf",
|
||||
// "resources.resource-nice-id.auth.sso-enabled": "true",
|
||||
// "resources.resource-nice-id.auth.sso-roles[0]": "Member",
|
||||
// "resources.resource-nice-id.auth.sso-users[0]": "owen@fossorial.io",
|
||||
// "resources.resource-nice-id.auth.whitelist-users[0]": "owen@fossorial.io",
|
||||
// "resources.resource-nice-id.auth.sso-users[0]": "owen@pangolin.net",
|
||||
// "resources.resource-nice-id.auth.whitelist-users[0]": "owen@pangolin.net",
|
||||
// "resources.resource-nice-id.targets[0].hostname": "localhost",
|
||||
// "resources.resource-nice-id.targets[0].method": "http",
|
||||
// "resources.resource-nice-id.targets[0].port": "8000",
|
||||
|
||||
@@ -3,11 +3,9 @@ import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
|
||||
import { db } from "@server/db";
|
||||
import { SupporterKey, supporterKey } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { license } from "#dynamic/license/license";
|
||||
import { configSchema, readConfigFile } from "./readConfigFile";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { build } from "@server/build";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export class Config {
|
||||
private rawConfig!: z.infer<typeof configSchema>;
|
||||
@@ -103,16 +101,10 @@ export class Config {
|
||||
throw new Error("Config not loaded. Call load() first.");
|
||||
}
|
||||
|
||||
license.setServerSecret(this.rawConfig.server.secret!);
|
||||
|
||||
await this.checkKeyStatus();
|
||||
}
|
||||
|
||||
private async checkKeyStatus() {
|
||||
if (build === "enterprise") {
|
||||
await license.check();
|
||||
}
|
||||
|
||||
if (build == "oss") {
|
||||
this.checkSupporterKey();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import { db, exitNodes, Transaction } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { ExitNodePingResult } from "@server/routers/newt";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -59,7 +59,11 @@ export function selectBestExitNode(
|
||||
return pingResults[0];
|
||||
}
|
||||
|
||||
export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
|
||||
export async function checkExitNodeOrg(
|
||||
exitNodeId: number,
|
||||
orgId: string,
|
||||
trx?: Transaction | typeof db
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -314,14 +314,11 @@ export const configSchema = z
|
||||
nameservers: z
|
||||
.array(z.string().optional().optional())
|
||||
.optional()
|
||||
.default(["ns1.fossorial.io", "ns2.fossorial.io"]),
|
||||
cname_extension: z.string().optional().default("fossorial.io")
|
||||
.default(["ns1.pangolin.net", "ns2.pangolin.net", "ns3.pangolin.net"]),
|
||||
cname_extension: z.string().optional().default("cname.pangolin.net")
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
nameservers: ["ns1.fossorial.io", "ns2.fossorial.io"],
|
||||
cname_extension: "fossorial.io"
|
||||
})
|
||||
.default({})
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -392,7 +389,7 @@ export function readConfigFile() {
|
||||
|
||||
if (!environment) {
|
||||
throw new Error(
|
||||
"No configuration file found. Please create one. https://docs.digpangolin.com/self-host/advanced/config-file"
|
||||
"No configuration file found. Please create one. https://docs.pangolin.net/self-host/advanced/config-file"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class TelemetryClient {
|
||||
this.client = new PostHog(
|
||||
"phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX",
|
||||
{
|
||||
host: "https://digpangolin.com/relay-O7yI"
|
||||
host: "https://pangolin.net/relay-O7yI"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -48,11 +48,11 @@ class TelemetryClient {
|
||||
this.startAnalyticsInterval();
|
||||
|
||||
logger.info(
|
||||
"Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.digpangolin.com/telemetry"
|
||||
"Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.pangolin.net/telemetry"
|
||||
);
|
||||
} else if (!this.enabled) {
|
||||
logger.info(
|
||||
"Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.digpangolin.com/telemetry"
|
||||
"Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.pangolin.net/telemetry"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
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 config from "@server/lib/config";
|
||||
import { resources, sites, Target, targets } from "@server/db";
|
||||
@@ -78,7 +88,7 @@ export async function getTraefikConfig(
|
||||
and(
|
||||
eq(targets.enabled, true),
|
||||
eq(resources.enabled, true),
|
||||
or(eq(sites.exitNodeId, exitNodeId), isNull(sites.exitNodeId)),
|
||||
eq(sites.exitNodeId, exitNodeId),
|
||||
or(
|
||||
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||
|
||||
@@ -13,61 +13,21 @@
|
||||
|
||||
import config from "./config";
|
||||
import { certificates, db } from "@server/db";
|
||||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import { and, eq, isNotNull, or, inArray, sql } from "drizzle-orm";
|
||||
import { decryptData } from "@server/lib/encryption";
|
||||
import * as fs from "fs";
|
||||
import NodeCache from "node-cache";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export async function getValidCertificatesForDomains(
|
||||
domains: Set<string>
|
||||
): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
wildcard: boolean | null;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: number | null;
|
||||
updatedAt?: number | null;
|
||||
}>
|
||||
> {
|
||||
if (domains.size === 0) {
|
||||
return [];
|
||||
let encryptionKeyPath = "";
|
||||
let encryptionKeyHex = "";
|
||||
let encryptionKey: Buffer;
|
||||
function loadEncryptData() {
|
||||
if (encryptionKey) {
|
||||
return; // already loaded
|
||||
}
|
||||
|
||||
const domainArray = Array.from(domains);
|
||||
|
||||
// TODO: add more foreign keys to make this query more efficient - we dont need to keep getting every certificate
|
||||
const validCerts = await db
|
||||
.select({
|
||||
id: certificates.certId,
|
||||
domain: certificates.domain,
|
||||
certFile: certificates.certFile,
|
||||
keyFile: certificates.keyFile,
|
||||
expiresAt: certificates.expiresAt,
|
||||
updatedAt: certificates.updatedAt,
|
||||
wildcard: certificates.wildcard
|
||||
})
|
||||
.from(certificates)
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.status, "valid"),
|
||||
isNotNull(certificates.certFile),
|
||||
isNotNull(certificates.keyFile)
|
||||
)
|
||||
);
|
||||
|
||||
// Filter certificates for the specified domains and if it is a wildcard then you can match on everything up to the first dot
|
||||
const validCertsFiltered = validCerts.filter((cert) => {
|
||||
return (
|
||||
domainArray.includes(cert.domain) ||
|
||||
(cert.wildcard &&
|
||||
domainArray.some((domain) =>
|
||||
domain.endsWith(`.${cert.domain}`)
|
||||
))
|
||||
);
|
||||
});
|
||||
|
||||
const encryptionKeyPath = config.getRawPrivateConfig().server.encryption_key_path;
|
||||
encryptionKeyPath = config.getRawPrivateConfig().server.encryption_key_path;
|
||||
|
||||
if (!fs.existsSync(encryptionKeyPath)) {
|
||||
throw new Error(
|
||||
@@ -75,10 +35,164 @@ export async function getValidCertificatesForDomains(
|
||||
);
|
||||
}
|
||||
|
||||
const encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim();
|
||||
const encryptionKey = Buffer.from(encryptionKeyHex, "hex");
|
||||
encryptionKeyHex = fs.readFileSync(encryptionKeyPath, "utf8").trim();
|
||||
encryptionKey = Buffer.from(encryptionKeyHex, "hex");
|
||||
}
|
||||
|
||||
const validCertsDecrypted = validCertsFiltered.map((cert) => {
|
||||
// Define the return type for clarity and type safety
|
||||
export type CertificateResult = {
|
||||
id: number;
|
||||
domain: string;
|
||||
queriedDomain: string; // The domain that was originally requested (may differ for wildcards)
|
||||
wildcard: boolean | null;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: number | null;
|
||||
updatedAt?: number | null;
|
||||
};
|
||||
|
||||
// --- In-Memory Cache Implementation ---
|
||||
const certificateCache = new NodeCache({ stdTTL: 180 }); // Cache for 3 minutes (180 seconds)
|
||||
|
||||
export async function getValidCertificatesForDomains(
|
||||
domains: Set<string>,
|
||||
useCache: boolean = true
|
||||
): Promise<Array<CertificateResult>> {
|
||||
|
||||
loadEncryptData(); // Ensure encryption key is loaded
|
||||
|
||||
const finalResults: CertificateResult[] = [];
|
||||
const domainsToQuery = new Set<string>();
|
||||
|
||||
// 1. Check cache first if enabled
|
||||
if (useCache) {
|
||||
for (const domain of domains) {
|
||||
const cachedCert = certificateCache.get<CertificateResult>(domain);
|
||||
if (cachedCert) {
|
||||
finalResults.push(cachedCert); // Valid cache hit
|
||||
} else {
|
||||
domainsToQuery.add(domain); // Cache miss or expired
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If caching is disabled, add all domains to the query set
|
||||
domains.forEach((d) => domainsToQuery.add(d));
|
||||
}
|
||||
|
||||
// 2. If all domains were resolved from the cache, return early
|
||||
if (domainsToQuery.size === 0) {
|
||||
const decryptedResults = decryptFinalResults(finalResults);
|
||||
return decryptedResults;
|
||||
}
|
||||
|
||||
// 3. Prepare domains for the database query
|
||||
const domainsToQueryArray = Array.from(domainsToQuery);
|
||||
const parentDomainsToQuery = new Set<string>();
|
||||
|
||||
domainsToQueryArray.forEach((domain) => {
|
||||
const parts = domain.split(".");
|
||||
// A wildcard can only match a domain with at least two parts (e.g., example.com)
|
||||
if (parts.length > 1) {
|
||||
parentDomainsToQuery.add(parts.slice(1).join("."));
|
||||
}
|
||||
});
|
||||
|
||||
const parentDomainsArray = Array.from(parentDomainsToQuery);
|
||||
|
||||
// 4. Build and execute a single, efficient Drizzle query
|
||||
// This query fetches all potential exact and wildcard matches in one database round-trip.
|
||||
const potentialCerts = await db
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.status, "valid"),
|
||||
isNotNull(certificates.certFile),
|
||||
isNotNull(certificates.keyFile),
|
||||
or(
|
||||
// Condition for exact matches on the requested domains
|
||||
inArray(certificates.domain, domainsToQueryArray),
|
||||
// Condition for wildcard matches on the parent domains
|
||||
parentDomainsArray.length > 0
|
||||
? and(
|
||||
inArray(certificates.domain, parentDomainsArray),
|
||||
eq(certificates.wildcard, true)
|
||||
)
|
||||
: // If there are no possible parent domains, this condition is false
|
||||
sql`false`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// 5. Process the database results, prioritizing exact matches over wildcards
|
||||
const exactMatches = new Map<string, (typeof potentialCerts)[0]>();
|
||||
const wildcardMatches = new Map<string, (typeof potentialCerts)[0]>();
|
||||
|
||||
for (const cert of potentialCerts) {
|
||||
if (cert.wildcard) {
|
||||
wildcardMatches.set(cert.domain, cert);
|
||||
} else {
|
||||
exactMatches.set(cert.domain, cert);
|
||||
}
|
||||
}
|
||||
|
||||
for (const domain of domainsToQuery) {
|
||||
let foundCert: (typeof potentialCerts)[0] | undefined = undefined;
|
||||
|
||||
// Priority 1: Check for an exact match (non-wildcard)
|
||||
if (exactMatches.has(domain)) {
|
||||
foundCert = exactMatches.get(domain);
|
||||
}
|
||||
// Priority 2: Check for a wildcard certificate that matches the exact domain
|
||||
else {
|
||||
if (wildcardMatches.has(domain)) {
|
||||
foundCert = wildcardMatches.get(domain);
|
||||
}
|
||||
// Priority 3: Check for a wildcard match on the parent domain
|
||||
else {
|
||||
const parts = domain.split(".");
|
||||
if (parts.length > 1) {
|
||||
const parentDomain = parts.slice(1).join(".");
|
||||
if (wildcardMatches.has(parentDomain)) {
|
||||
foundCert = wildcardMatches.get(parentDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a certificate was found, format it, add to results, and cache it
|
||||
if (foundCert) {
|
||||
logger.debug(
|
||||
`Creating result cert for ${domain} using cert from ${foundCert.domain}`
|
||||
);
|
||||
const resultCert: CertificateResult = {
|
||||
id: foundCert.certId,
|
||||
domain: foundCert.domain, // The actual domain of the cert record
|
||||
queriedDomain: domain, // The domain that was originally requested
|
||||
wildcard: foundCert.wildcard,
|
||||
certFile: foundCert.certFile,
|
||||
keyFile: foundCert.keyFile,
|
||||
expiresAt: foundCert.expiresAt,
|
||||
updatedAt: foundCert.updatedAt
|
||||
};
|
||||
|
||||
finalResults.push(resultCert);
|
||||
|
||||
// Add to cache for future requests, using the *requested domain* as the key
|
||||
if (useCache) {
|
||||
certificateCache.set(domain, resultCert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decryptedResults = decryptFinalResults(finalResults);
|
||||
return decryptedResults;
|
||||
}
|
||||
|
||||
function decryptFinalResults(
|
||||
finalResults: CertificateResult[]
|
||||
): CertificateResult[] {
|
||||
const validCertsDecrypted = finalResults.map((cert) => {
|
||||
// Decrypt and save certificate file
|
||||
const decryptedCert = decryptData(
|
||||
cert.certFile!, // is not null from query
|
||||
@@ -97,4 +211,4 @@ export async function getValidCertificatesForDomains(
|
||||
});
|
||||
|
||||
return validCertsDecrypted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
privateConfigSchema,
|
||||
readPrivateConfigFile
|
||||
} from "#private/lib/readConfigFile";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export class PrivateConfig {
|
||||
private rawPrivateConfig!: z.infer<typeof privateConfigSchema>;
|
||||
@@ -44,115 +43,104 @@ export class PrivateConfig {
|
||||
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(
|
||||
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 =
|
||||
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 =
|
||||
parsedPrivateConfig.branding?.logo?.dark_path || undefined;
|
||||
this.rawPrivateConfig.branding?.logo?.dark_path || undefined;
|
||||
}
|
||||
|
||||
if (build != "oss") {
|
||||
if (parsedPrivateConfig.branding?.logo?.light_path) {
|
||||
process.env.BRANDING_LOGO_LIGHT_PATH =
|
||||
parsedPrivateConfig.branding?.logo?.light_path;
|
||||
}
|
||||
if (parsedPrivateConfig.branding?.logo?.dark_path) {
|
||||
process.env.BRANDING_LOGO_DARK_PATH =
|
||||
parsedPrivateConfig.branding?.logo?.dark_path || undefined;
|
||||
}
|
||||
process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding
|
||||
?.logo?.auth_page?.width
|
||||
? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString()
|
||||
: undefined;
|
||||
process.env.BRANDING_LOGO_AUTH_HEIGHT = this.rawPrivateConfig.branding
|
||||
?.logo?.auth_page?.height
|
||||
? this.rawPrivateConfig.branding?.logo?.auth_page?.height.toString()
|
||||
: undefined;
|
||||
|
||||
process.env.BRANDING_LOGO_AUTH_WIDTH = parsedPrivateConfig.branding
|
||||
?.logo?.auth_page?.width
|
||||
? parsedPrivateConfig.branding?.logo?.auth_page?.width.toString()
|
||||
: undefined;
|
||||
process.env.BRANDING_LOGO_AUTH_HEIGHT = parsedPrivateConfig.branding
|
||||
?.logo?.auth_page?.height
|
||||
? parsedPrivateConfig.branding?.logo?.auth_page?.height.toString()
|
||||
: undefined;
|
||||
process.env.BRANDING_LOGO_NAVBAR_WIDTH = this.rawPrivateConfig.branding
|
||||
?.logo?.navbar?.width
|
||||
? this.rawPrivateConfig.branding?.logo?.navbar?.width.toString()
|
||||
: undefined;
|
||||
process.env.BRANDING_LOGO_NAVBAR_HEIGHT = this.rawPrivateConfig.branding
|
||||
?.logo?.navbar?.height
|
||||
? this.rawPrivateConfig.branding?.logo?.navbar?.height.toString()
|
||||
: undefined;
|
||||
|
||||
process.env.BRANDING_LOGO_NAVBAR_WIDTH = parsedPrivateConfig
|
||||
.branding?.logo?.navbar?.width
|
||||
? 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 =
|
||||
this.rawPrivateConfig.branding?.favicon_path;
|
||||
|
||||
process.env.BRANDING_FAVICON_PATH =
|
||||
parsedPrivateConfig.branding?.favicon_path;
|
||||
process.env.BRANDING_APP_NAME =
|
||||
this.rawPrivateConfig.branding?.app_name || "Pangolin";
|
||||
|
||||
process.env.BRANDING_APP_NAME =
|
||||
parsedPrivateConfig.branding?.app_name || "Pangolin";
|
||||
|
||||
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();
|
||||
}
|
||||
if (this.rawPrivateConfig.branding?.footer) {
|
||||
process.env.BRANDING_FOOTER = JSON.stringify(
|
||||
this.rawPrivateConfig.branding?.footer
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
resources,
|
||||
targets,
|
||||
sites,
|
||||
targetHealthCheck
|
||||
targetHealthCheck,
|
||||
Transaction
|
||||
} from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { ExitNodePingResult } from "@server/routers/newt";
|
||||
@@ -183,47 +184,47 @@ export async function listExitNodes(orgId: string, filterOnline = false, noCloud
|
||||
return [];
|
||||
}
|
||||
|
||||
// Enhanced online checking: consider node offline if either DB says offline OR HTTP ping fails
|
||||
const nodesWithRealOnlineStatus = await Promise.all(
|
||||
allExitNodes.map(async (node) => {
|
||||
// If database says it's online, verify with HTTP ping
|
||||
let online: boolean;
|
||||
if (filterOnline && node.type == "remoteExitNode") {
|
||||
try {
|
||||
const isActuallyOnline = await checkExitNodeOnlineStatus(
|
||||
node.endpoint
|
||||
);
|
||||
// // Enhanced online checking: consider node offline if either DB says offline OR HTTP ping fails
|
||||
// const nodesWithRealOnlineStatus = await Promise.all(
|
||||
// allExitNodes.map(async (node) => {
|
||||
// // If database says it's online, verify with HTTP ping
|
||||
// let online: boolean;
|
||||
// if (filterOnline && node.type == "remoteExitNode") {
|
||||
// try {
|
||||
// const isActuallyOnline = await checkExitNodeOnlineStatus(
|
||||
// node.endpoint
|
||||
// );
|
||||
|
||||
// set the item in the database if it is offline
|
||||
if (isActuallyOnline != node.online) {
|
||||
await db
|
||||
.update(exitNodes)
|
||||
.set({ online: isActuallyOnline })
|
||||
.where(eq(exitNodes.exitNodeId, node.exitNodeId));
|
||||
}
|
||||
online = isActuallyOnline;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to check online status for exit node ${node.name} (${node.endpoint}): ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
);
|
||||
online = false;
|
||||
}
|
||||
} else {
|
||||
online = node.online;
|
||||
}
|
||||
// // set the item in the database if it is offline
|
||||
// if (isActuallyOnline != node.online) {
|
||||
// await db
|
||||
// .update(exitNodes)
|
||||
// .set({ online: isActuallyOnline })
|
||||
// .where(eq(exitNodes.exitNodeId, node.exitNodeId));
|
||||
// }
|
||||
// online = isActuallyOnline;
|
||||
// } catch (error) {
|
||||
// logger.warn(
|
||||
// `Failed to check online status for exit node ${node.name} (${node.endpoint}): ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
// );
|
||||
// online = false;
|
||||
// }
|
||||
// } else {
|
||||
// online = node.online;
|
||||
// }
|
||||
|
||||
return {
|
||||
...node,
|
||||
online
|
||||
};
|
||||
})
|
||||
);
|
||||
// return {
|
||||
// ...node,
|
||||
// online
|
||||
// };
|
||||
// })
|
||||
// );
|
||||
|
||||
const remoteExitNodes = nodesWithRealOnlineStatus.filter(
|
||||
const remoteExitNodes = allExitNodes.filter(
|
||||
(node) =>
|
||||
node.type === "remoteExitNode" && (!filterOnline || node.online)
|
||||
);
|
||||
const gerbilExitNodes = nodesWithRealOnlineStatus.filter(
|
||||
const gerbilExitNodes = allExitNodes.filter(
|
||||
(node) => node.type === "gerbil" && (!filterOnline || node.online) && !noCloud
|
||||
);
|
||||
|
||||
@@ -333,8 +334,8 @@ export function selectBestExitNode(
|
||||
return fallbackNode;
|
||||
}
|
||||
|
||||
export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
|
||||
const [exitNodeOrg] = await db
|
||||
export async function checkExitNodeOrg(exitNodeId: number, orgId: string, trx: Transaction | typeof db = db) {
|
||||
const [exitNodeOrg] = await trx
|
||||
.select()
|
||||
.from(exitNodeOrgs)
|
||||
.where(
|
||||
|
||||
@@ -76,7 +76,7 @@ export const privateConfigSchema = z.object({
|
||||
local_exit_node_reachable_at: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("http://gerbil:3003")
|
||||
.default("http://gerbil:3004")
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
@@ -172,6 +172,15 @@ export function readPrivateConfigFile() {
|
||||
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) => {
|
||||
try {
|
||||
const yamlContent = fs.readFileSync(configPath, "utf8");
|
||||
|
||||
@@ -19,13 +19,27 @@ import {
|
||||
loginPage,
|
||||
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 config from "@server/lib/config";
|
||||
import { orgs, resources, sites, Target, targets } from "@server/db";
|
||||
import { sanitize, validatePathRewriteConfig } from "@server/lib/traefik/utils";
|
||||
import privateConfig from "#private/lib/config";
|
||||
import createPathRewriteMiddleware from "@server/lib/traefik/middleware";
|
||||
import {
|
||||
CertificateResult,
|
||||
getValidCertificatesForDomains
|
||||
} from "#private/lib/certificates";
|
||||
|
||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||
const redirectToRootMiddlewareName = "redirect-to-root";
|
||||
@@ -89,14 +103,11 @@ export async function getTraefikConfig(
|
||||
subnet: sites.subnet,
|
||||
exitNodeId: sites.exitNodeId,
|
||||
// Namespace
|
||||
domainNamespaceId: domainNamespaces.domainNamespaceId,
|
||||
// Certificate
|
||||
certificateStatus: certificates.status
|
||||
domainNamespaceId: domainNamespaces.domainNamespaceId
|
||||
})
|
||||
.from(sites)
|
||||
.innerJoin(targets, eq(targets.siteId, sites.siteId))
|
||||
.innerJoin(resources, eq(resources.resourceId, targets.resourceId))
|
||||
.leftJoin(certificates, eq(certificates.domainId, resources.domainId))
|
||||
.leftJoin(
|
||||
targetHealthCheck,
|
||||
eq(targetHealthCheck.targetId, targets.targetId)
|
||||
@@ -109,10 +120,7 @@ export async function getTraefikConfig(
|
||||
and(
|
||||
eq(targets.enabled, true),
|
||||
eq(resources.enabled, true),
|
||||
// or(
|
||||
eq(sites.exitNodeId, exitNodeId),
|
||||
// isNull(sites.exitNodeId)
|
||||
// ),
|
||||
or(
|
||||
ne(targetHealthCheck.hcHealth, "unhealthy"), // Exclude unhealthy targets
|
||||
isNull(targetHealthCheck.hcHealth) // Include targets with no health check record
|
||||
@@ -183,7 +191,6 @@ export async function getTraefikConfig(
|
||||
tlsServerName: row.tlsServerName,
|
||||
setHostHeader: row.setHostHeader,
|
||||
enableProxy: row.enableProxy,
|
||||
certificateStatus: row.certificateStatus,
|
||||
targets: [],
|
||||
headers: row.headers,
|
||||
path: row.path, // the targets will all have the same path
|
||||
@@ -213,6 +220,20 @@ export async function getTraefikConfig(
|
||||
});
|
||||
});
|
||||
|
||||
let validCerts: CertificateResult[] = [];
|
||||
if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
// create a list of all domains to get certs for
|
||||
const domains = new Set<string>();
|
||||
for (const resource of resourcesMap.values()) {
|
||||
if (resource.enabled && resource.ssl && resource.fullDomain) {
|
||||
domains.add(resource.fullDomain);
|
||||
}
|
||||
}
|
||||
// get the valid certs for these domains
|
||||
validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often
|
||||
// logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`);
|
||||
}
|
||||
|
||||
const config_output: any = {
|
||||
http: {
|
||||
middlewares: {
|
||||
@@ -255,14 +276,6 @@ export async function getTraefikConfig(
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail
|
||||
// if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
// logger.debug(
|
||||
// `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
|
||||
// );
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// add routers and services empty objects if they don't exist
|
||||
if (!config_output.http.routers) {
|
||||
config_output.http.routers = {};
|
||||
@@ -272,22 +285,22 @@ export async function getTraefikConfig(
|
||||
config_output.http.services = {};
|
||||
}
|
||||
|
||||
const domainParts = fullDomain.split(".");
|
||||
let wildCard;
|
||||
if (domainParts.length <= 2) {
|
||||
wildCard = `*.${domainParts.join(".")}`;
|
||||
} else {
|
||||
wildCard = `*.${domainParts.slice(1).join(".")}`;
|
||||
}
|
||||
|
||||
if (!resource.subdomain) {
|
||||
wildCard = resource.fullDomain;
|
||||
}
|
||||
|
||||
const configDomain = config.getDomain(resource.domainId);
|
||||
|
||||
let tls = {};
|
||||
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
const domainParts = fullDomain.split(".");
|
||||
let wildCard;
|
||||
if (domainParts.length <= 2) {
|
||||
wildCard = `*.${domainParts.join(".")}`;
|
||||
} else {
|
||||
wildCard = `*.${domainParts.slice(1).join(".")}`;
|
||||
}
|
||||
|
||||
if (!resource.subdomain) {
|
||||
wildCard = resource.fullDomain;
|
||||
}
|
||||
|
||||
const configDomain = config.getDomain(resource.domainId);
|
||||
|
||||
let certResolver: string, preferWildcardCert: boolean;
|
||||
if (!configDomain) {
|
||||
certResolver = config.getRawConfig().traefik.cert_resolver;
|
||||
@@ -310,6 +323,17 @@ export async function getTraefikConfig(
|
||||
}
|
||||
: {})
|
||||
};
|
||||
} else {
|
||||
// find a cert that matches the full domain, if not continue
|
||||
const matchingCert = validCerts.find(
|
||||
(cert) => cert.queriedDomain === resource.fullDomain
|
||||
);
|
||||
if (!matchingCert) {
|
||||
logger.warn(
|
||||
`No matching certificate found for domain: ${resource.fullDomain}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const additionalMiddlewares =
|
||||
@@ -676,20 +700,31 @@ export async function getTraefikConfig(
|
||||
loginPageId: loginPage.loginPageId,
|
||||
fullDomain: loginPage.fullDomain,
|
||||
exitNodeId: exitNodes.exitNodeId,
|
||||
domainId: loginPage.domainId,
|
||||
certificateStatus: certificates.status
|
||||
domainId: loginPage.domainId
|
||||
})
|
||||
.from(loginPage)
|
||||
.innerJoin(
|
||||
exitNodes,
|
||||
eq(exitNodes.exitNodeId, loginPage.exitNodeId)
|
||||
)
|
||||
.leftJoin(
|
||||
certificates,
|
||||
eq(certificates.domainId, loginPage.domainId)
|
||||
)
|
||||
.where(eq(exitNodes.exitNodeId, exitNodeId));
|
||||
|
||||
let validCertsLoginPages: CertificateResult[] = [];
|
||||
if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
|
||||
// create a list of all domains to get certs for
|
||||
const domains = new Set<string>();
|
||||
for (const lp of exitNodeLoginPages) {
|
||||
if (lp.fullDomain) {
|
||||
domains.add(lp.fullDomain);
|
||||
}
|
||||
}
|
||||
// get the valid certs for these domains
|
||||
validCertsLoginPages = await getValidCertificatesForDomains(
|
||||
domains,
|
||||
true
|
||||
); // we are caching here because this is called often
|
||||
}
|
||||
|
||||
if (exitNodeLoginPages.length > 0) {
|
||||
if (!config_output.http.services) {
|
||||
config_output.http.services = {};
|
||||
@@ -719,8 +754,22 @@ export async function getTraefikConfig(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lp.certificateStatus !== "valid") {
|
||||
continue;
|
||||
let tls = {};
|
||||
if (
|
||||
!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns
|
||||
) {
|
||||
// TODO: we need to add the wildcard logic here too
|
||||
} else {
|
||||
// find a cert that matches the full domain, if not continue
|
||||
const matchingCert = validCertsLoginPages.find(
|
||||
(cert) => cert.queriedDomain === lp.fullDomain
|
||||
);
|
||||
if (!matchingCert) {
|
||||
logger.warn(
|
||||
`No matching certificate found for login page domain: ${lp.fullDomain}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// auth-allowed:
|
||||
@@ -743,7 +792,7 @@ export async function getTraefikConfig(
|
||||
service: "landing-service",
|
||||
rule: `Host(\`${fullDomain}\`) && (PathRegexp(\`^/auth/resource/[^/]+$\`) || PathRegexp(\`^/auth/idp/[0-9]+/oidc/callback\`) || PathPrefix(\`/_next\`) || Path(\`/auth/org\`) || PathRegexp(\`^/__nextjs*\`))`,
|
||||
priority: 203,
|
||||
tls: {}
|
||||
tls: tls
|
||||
};
|
||||
|
||||
// auth-catchall:
|
||||
@@ -762,7 +811,7 @@ export async function getTraefikConfig(
|
||||
service: "landing-service",
|
||||
rule: `Host(\`${fullDomain}\`)`,
|
||||
priority: 202,
|
||||
tls: {}
|
||||
tls: tls
|
||||
};
|
||||
|
||||
// we need to add a redirect from http to https too
|
||||
|
||||
@@ -61,7 +61,17 @@ export async function createExitNode(
|
||||
`Created new exit node ${exitNode.name} with address ${exitNode.address} and port ${exitNode.listenPort}`
|
||||
);
|
||||
} else {
|
||||
exitNode = exitNodeQuery;
|
||||
// update the reachable at
|
||||
[exitNode] = await db
|
||||
.update(exitNodes)
|
||||
.set({
|
||||
reachableAt,
|
||||
online: true
|
||||
})
|
||||
.where(eq(exitNodes.exitNodeId, exitNodeQuery.exitNodeId))
|
||||
.returning();
|
||||
|
||||
logger.info(`Updated exit node reachableAt to ${reachableAt}`);
|
||||
}
|
||||
|
||||
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)
|
||||
hybridRouter.get(
|
||||
"/certificates/domains",
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
loadEncryptData(); // Ensure encryption key is loaded
|
||||
|
||||
const parsed = getCertificatesByDomainsQuerySchema.safeParse(
|
||||
req.query
|
||||
);
|
||||
@@ -425,20 +447,6 @@ hybridRouter.get(
|
||||
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) => {
|
||||
// Decrypt and save certificate file
|
||||
const decryptedCert = decryptData(
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function createRemoteExitNode(
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
|
||||
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@pangolin.net"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,12 +47,13 @@ export async function createExitNode(publicKey: string, reachableAt: string | un
|
||||
.update(exitNodes)
|
||||
.set({
|
||||
reachableAt,
|
||||
publicKey
|
||||
publicKey,
|
||||
online: true
|
||||
})
|
||||
.where(eq(exitNodes.publicKey, publicKey))
|
||||
.returning();
|
||||
|
||||
logger.info(`Updated exit node`);
|
||||
logger.info(`Updated exit node with reachableAt to ${reachableAt}`);
|
||||
}
|
||||
|
||||
return exitNode;
|
||||
|
||||
@@ -117,27 +117,4 @@ export async function generateGerbilConfig(exitNode: ExitNode) {
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,8 @@ export async function updateSiteBandwidth(
|
||||
if (
|
||||
await checkExitNodeOrg(
|
||||
exitNodeId,
|
||||
updatedSite.orgId
|
||||
updatedSite.orgId,
|
||||
trx
|
||||
)
|
||||
) {
|
||||
// not allowed
|
||||
@@ -148,7 +149,8 @@ export async function updateSiteBandwidth(
|
||||
orgId,
|
||||
true,
|
||||
FeatureId.EGRESS_DATA_MB,
|
||||
bandwidthUsage
|
||||
bandwidthUsage,
|
||||
trx
|
||||
)
|
||||
.catch((error: any) => {
|
||||
logger.error(
|
||||
@@ -174,7 +176,8 @@ export async function updateSiteBandwidth(
|
||||
orgId,
|
||||
true,
|
||||
FeatureId.SITE_UPTIME,
|
||||
uptimeUsage
|
||||
uptimeUsage,
|
||||
trx
|
||||
)
|
||||
.catch((error: any) => {
|
||||
logger.error(
|
||||
@@ -242,7 +245,8 @@ export async function updateSiteBandwidth(
|
||||
if (
|
||||
await checkExitNodeOrg(
|
||||
exitNodeId,
|
||||
updatedSite.orgId
|
||||
updatedSite.orgId,
|
||||
trx
|
||||
)
|
||||
) {
|
||||
// not allowed
|
||||
|
||||
@@ -414,6 +414,20 @@ authenticated.post(
|
||||
resource.setResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/whitelist/add`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
resource.addEmailToResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/whitelist/remove`,
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
||||
resource.removeEmailFromResourceWhitelist
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/resource/:resourceId/whitelist`,
|
||||
verifyApiKeyResourceAccess,
|
||||
|
||||
147
server/routers/resource/addEmailToResourceWhitelist.ts
Normal file
147
server/routers/resource/addEmailToResourceWhitelist.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources, resourceWhitelist } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const addEmailToResourceWhitelistBodySchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.or(
|
||||
z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
|
||||
message:
|
||||
"Invalid email address. Wildcard (*) must be the entire local part."
|
||||
})
|
||||
)
|
||||
.transform((v) => v.toLowerCase())
|
||||
})
|
||||
.strict();
|
||||
|
||||
const addEmailToResourceWhitelistParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/whitelist/add",
|
||||
description: "Add a single email to the resource whitelist.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: addEmailToResourceWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: addEmailToResourceWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function addEmailToResourceWhitelist(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = addEmailToResourceWhitelistBodySchema.safeParse(
|
||||
req.body
|
||||
);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { email } = parsedBody.data;
|
||||
|
||||
const parsedParams = addEmailToResourceWhitelistParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId));
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.emailWhitelistEnabled) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Email whitelist is not enabled for this resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if email already exists in whitelist
|
||||
const existingEntry = await db
|
||||
.select()
|
||||
.from(resourceWhitelist)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceWhitelist.resourceId, resourceId),
|
||||
eq(resourceWhitelist.email, email)
|
||||
)
|
||||
);
|
||||
|
||||
if (existingEntry.length > 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"Email already exists in whitelist"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.insert(resourceWhitelist).values({
|
||||
email,
|
||||
resourceId
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Email added to whitelist successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,3 +23,5 @@ export * from "./listResourceRules";
|
||||
export * from "./updateResourceRule";
|
||||
export * from "./getUserResources";
|
||||
export * from "./setResourceHeaderAuth";
|
||||
export * from "./addEmailToResourceWhitelist";
|
||||
export * from "./removeEmailFromResourceWhitelist";
|
||||
|
||||
150
server/routers/resource/removeEmailFromResourceWhitelist.ts
Normal file
150
server/routers/resource/removeEmailFromResourceWhitelist.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db } from "@server/db";
|
||||
import { resources, resourceWhitelist } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const removeEmailFromResourceWhitelistBodySchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.email()
|
||||
.or(
|
||||
z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, {
|
||||
message:
|
||||
"Invalid email address. Wildcard (*) must be the entire local part."
|
||||
})
|
||||
)
|
||||
.transform((v) => v.toLowerCase())
|
||||
})
|
||||
.strict();
|
||||
|
||||
const removeEmailFromResourceWhitelistParamsSchema = z
|
||||
.object({
|
||||
resourceId: z
|
||||
.string()
|
||||
.transform(Number)
|
||||
.pipe(z.number().int().positive())
|
||||
})
|
||||
.strict();
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/resource/{resourceId}/whitelist/remove",
|
||||
description: "Remove a single email from the resource whitelist.",
|
||||
tags: [OpenAPITags.Resource],
|
||||
request: {
|
||||
params: removeEmailFromResourceWhitelistParamsSchema,
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: removeEmailFromResourceWhitelistBodySchema
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function removeEmailFromResourceWhitelist(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = removeEmailFromResourceWhitelistBodySchema.safeParse(
|
||||
req.body
|
||||
);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { email } = parsedBody.data;
|
||||
|
||||
const parsedParams =
|
||||
removeEmailFromResourceWhitelistParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select()
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId));
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.emailWhitelistEnabled) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Email whitelist is not enabled for this resource"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if email exists in whitelist
|
||||
const existingEntry = await db
|
||||
.select()
|
||||
.from(resourceWhitelist)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceWhitelist.resourceId, resourceId),
|
||||
eq(resourceWhitelist.email, email)
|
||||
)
|
||||
);
|
||||
|
||||
if (existingEntry.length === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"Email not found in whitelist"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(resourceWhitelist)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceWhitelist.resourceId, resourceId),
|
||||
eq(resourceWhitelist.email, email)
|
||||
)
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Email removed from whitelist successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { hashPassword } from "@server/auth/password";
|
||||
import { isValidIP } from "@server/lib/validators";
|
||||
import { isIpInCidr } from "@server/lib/ip";
|
||||
import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes";
|
||||
import { build } from "@server/build";
|
||||
|
||||
const createSiteParamsSchema = z
|
||||
.object({
|
||||
@@ -203,10 +204,10 @@ export async function createSite(
|
||||
|
||||
const niceId = await getUniqueSiteName(orgId);
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
let newSite: Site;
|
||||
let newSite: Site;
|
||||
|
||||
if ((type == "wireguard" || type == "newt") && exitNodeId) {
|
||||
await db.transaction(async (trx) => {
|
||||
if (type == "wireguard" || type == "newt") {
|
||||
// we are creating a site with an exit node (tunneled)
|
||||
if (!subnet) {
|
||||
return next(
|
||||
@@ -217,11 +218,19 @@ export async function createSite(
|
||||
);
|
||||
}
|
||||
|
||||
const { exitNode, hasAccess } =
|
||||
await verifyExitNodeOrgAccess(
|
||||
exitNodeId,
|
||||
orgId
|
||||
if (!exitNodeId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Exit node ID is required for tunneled sites"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { exitNode, hasAccess } = await verifyExitNodeOrgAccess(
|
||||
exitNodeId,
|
||||
orgId
|
||||
);
|
||||
|
||||
if (!exitNode) {
|
||||
logger.warn("Exit node not found");
|
||||
@@ -257,13 +266,51 @@ export async function createSite(
|
||||
...(pubKey && type == "wireguard" && { pubKey })
|
||||
})
|
||||
.returning();
|
||||
} else {
|
||||
// we are creating a site with no tunneling
|
||||
} else if (type == "local") {
|
||||
let exitNodeIdToCreate = exitNodeId;
|
||||
if (!exitNodeIdToCreate) {
|
||||
if (build == "saas") {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Exit node ID of a remote node is required for local sites"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// select the exit node for local sites
|
||||
// TODO: THIS SHOULD BE CHOSEN IN THE FRONTEND OR SOMETHING BECAUSE
|
||||
// YOU CAN HAVE MORE THAN ONE NODE IN THE SYSTEM AND YOU SHOULD SELECT
|
||||
// WHICH GERBIL NODE TO PUT THE SITE ON BUT FOR NOW THIS WILL DO
|
||||
const [localExitNode] = await trx
|
||||
.select()
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.type, "gerbil"))
|
||||
.limit(1);
|
||||
|
||||
if (!localExitNode) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"No gerbil exit node found for organization. Please create a gerbil exit node first."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
exitNodeIdToCreate = localExitNode.exitNodeId;
|
||||
} else {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Site type not recognized"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
[newSite] = await trx
|
||||
.insert(sites)
|
||||
.values({
|
||||
exitNodeId: exitNodeId,
|
||||
exitNodeId: exitNodeIdToCreate,
|
||||
orgId,
|
||||
name,
|
||||
niceId,
|
||||
|
||||
@@ -46,15 +46,24 @@ const createTargetSchema = z
|
||||
.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(),
|
||||
hcMethod: z.string().min(1).optional().nullable(),
|
||||
hcStatus: z.number().int().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(),
|
||||
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
|
||||
priority: z.number().int().min(1).max(1000)
|
||||
rewritePathType: z
|
||||
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
priority: z.number().int().min(1).max(1000).optional().nullable()
|
||||
})
|
||||
.strict();
|
||||
|
||||
@@ -164,12 +173,14 @@ export async function createTarget(
|
||||
|
||||
let newTarget: Target[] = [];
|
||||
let healthCheck: TargetHealthCheck[] = [];
|
||||
let targetIps: string[] = [];
|
||||
if (site.type == "local") {
|
||||
newTarget = await db
|
||||
.insert(targets)
|
||||
.values({
|
||||
resourceId,
|
||||
...targetData
|
||||
...targetData,
|
||||
priority: targetData.priority || 100
|
||||
})
|
||||
.returning();
|
||||
} else {
|
||||
@@ -186,7 +197,7 @@ export async function createTarget(
|
||||
);
|
||||
}
|
||||
|
||||
const { internalPort, targetIps } = await pickPort(
|
||||
const { internalPort, targetIps: newTargetIps } = await pickPort(
|
||||
site.siteId!,
|
||||
db
|
||||
);
|
||||
@@ -214,61 +225,63 @@ export async function createTarget(
|
||||
pathMatchType: targetData.pathMatchType,
|
||||
rewritePath: targetData.rewritePath,
|
||||
rewritePathType: targetData.rewritePathType,
|
||||
priority: targetData.priority
|
||||
})
|
||||
.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"
|
||||
priority: targetData.priority || 100
|
||||
})
|
||||
.returning();
|
||||
|
||||
// add the new target to the targetIps array
|
||||
targetIps.push(`${targetData.ip}/32`);
|
||||
newTargetIps.push(`${targetData.ip}/32`);
|
||||
|
||||
if (site.pubKey) {
|
||||
if (site.type == "wireguard") {
|
||||
await addPeer(site.exitNodeId!, {
|
||||
publicKey: site.pubKey,
|
||||
allowedIps: targetIps.flat()
|
||||
});
|
||||
} else if (site.type == "newt") {
|
||||
// get the newt on the site by querying the newt table for siteId
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, site.siteId))
|
||||
.limit(1);
|
||||
targetIps = newTargetIps;
|
||||
}
|
||||
|
||||
await addTargets(
|
||||
newt.newtId,
|
||||
newTarget,
|
||||
healthCheck,
|
||||
resource.protocol,
|
||||
resource.proxyPort
|
||||
);
|
||||
}
|
||||
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();
|
||||
|
||||
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;
|
||||
}[];
|
||||
|
||||
// Delete the old record
|
||||
await db.execute(sql`
|
||||
DELETE FROM "webauthnCredentials";
|
||||
`);
|
||||
|
||||
for (const webauthnCredential of webauthnCredentials) {
|
||||
const newCredentialId = isoBase64URL.fromBuffer(
|
||||
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
|
||||
await db.execute(sql`
|
||||
INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated")
|
||||
|
||||
45
server/setup/scriptsPg/1.11.1.ts
Normal file
45
server/setup/scriptsPg/1.11.1.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { db } from "@server/db/pg/driver";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
const version = "1.11.1";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
try {
|
||||
// Get the first exit node with type 'gerbil'
|
||||
const exitNodesQuery = await db.execute(
|
||||
sql`SELECT "exitNodeId" FROM "exitNodes" WHERE "type" = 'gerbil' LIMIT 1`
|
||||
);
|
||||
const exitNodes = exitNodesQuery.rows as {
|
||||
exitNodeId: number;
|
||||
}[];
|
||||
|
||||
const exitNodeId = exitNodes.length > 0 ? exitNodes[0].exitNodeId : null;
|
||||
|
||||
// Get all sites with type 'local'
|
||||
const sitesQuery = await db.execute(
|
||||
sql`SELECT "siteId" FROM "sites" WHERE "type" = 'local'`
|
||||
);
|
||||
const sites = sitesQuery.rows as {
|
||||
siteId: number;
|
||||
}[];
|
||||
|
||||
// Update sites to use the exit node
|
||||
for (const site of sites) {
|
||||
await db.execute(sql`
|
||||
UPDATE "sites" SET "exitNodeId" = ${exitNodeId} WHERE "siteId" = ${site.siteId}
|
||||
`);
|
||||
}
|
||||
|
||||
await db.execute(sql`COMMIT`);
|
||||
console.log(`Updated sites with exit node`);
|
||||
} catch (e) {
|
||||
await db.execute(sql`ROLLBACK`);
|
||||
console.log("Unable to update sites with exit node");
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export default async function migration() {
|
||||
fs.writeFileSync(filePath, updatedYaml, "utf8");
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Failed to add resource_session_request_param to config. Please add it manually. https://docs.digpangolin.com/self-host/advanced/config-file`
|
||||
`Failed to add resource_session_request_param to config. Please add it manually. https://docs.pangolin.net/self-host/advanced/config-file`
|
||||
);
|
||||
trx.rollback();
|
||||
return;
|
||||
|
||||
@@ -269,6 +269,8 @@ export default async function migration() {
|
||||
dateCreated: string;
|
||||
}[];
|
||||
|
||||
db.prepare(`DELETE FROM 'webauthnCredentials';`).run();
|
||||
|
||||
for (const webauthnCredential of webauthnCredentials) {
|
||||
const newCredentialId = isoBase64URL.fromBuffer(
|
||||
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
|
||||
db.prepare(
|
||||
`INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
37
server/setup/scriptsSqlite/1.11.1.ts
Normal file
37
server/setup/scriptsSqlite/1.11.1.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { APP_PATH } from "@server/lib/consts";
|
||||
import Database from "better-sqlite3";
|
||||
import path from "path";
|
||||
|
||||
const version = "1.11.1";
|
||||
|
||||
export default async function migration() {
|
||||
console.log(`Running setup script ${version}...`);
|
||||
|
||||
const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||
const db = new Database(location);
|
||||
|
||||
db.transaction(() => {
|
||||
const exitNodes = db.prepare(`SELECT * FROM exitNodes WHERE type = 'gerbil' LIMIT 1`).all() as {
|
||||
exitNodeId: number;
|
||||
name: string;
|
||||
}[];
|
||||
|
||||
const exitNodeId = exitNodes.length > 0 ? exitNodes[0].exitNodeId : null;
|
||||
|
||||
// get all of the targets
|
||||
const sites = db.prepare(`SELECT * FROM sites WHERE type = 'local'`).all() as {
|
||||
siteId: number;
|
||||
exitNodeId: number | null;
|
||||
}[];
|
||||
|
||||
const defineExitNodeOnSite = db.prepare(
|
||||
`UPDATE sites SET exitNodeId = ? WHERE siteId = ?`
|
||||
);
|
||||
|
||||
for (const site of sites) {
|
||||
defineExitNodeOnSite.run(exitNodeId, site.siteId);
|
||||
}
|
||||
})();
|
||||
|
||||
console.log(`${version} migration complete`);
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export default async function migration() {
|
||||
console.log(`Added new config option: resource_access_token_headers`);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Unable to add new config option: resource_access_token_headers. Please add it manually. https://docs.digpangolin.com/self-host/advanced/config-file`
|
||||
`Unable to add new config option: resource_access_token_headers. Please add it manually. https://docs.pangolin.net/self-host/advanced/config-file`
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -401,7 +401,7 @@ export default function GeneralPage() {
|
||||
</Badge>
|
||||
<Link
|
||||
className="flex items-center gap-2 text-primary hover:underline"
|
||||
href="https://digpangolin.com/pricing"
|
||||
href="https://pangolin.net/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -270,17 +270,12 @@ export default function ExitNodesTable({
|
||||
setSelectedNode(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("remoteExitNodeQuestionRemove", {
|
||||
selectedNode:
|
||||
selectedNode?.name || selectedNode?.id
|
||||
})}
|
||||
{t("remoteExitNodeQuestionRemove")}
|
||||
</p>
|
||||
|
||||
<p>{t("remoteExitNodeMessageRemove")}</p>
|
||||
|
||||
<p>{t("remoteExitNodeMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("remoteExitNodeConfirmDelete")}
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
FaFreebsd,
|
||||
FaWindows
|
||||
} from "react-icons/fa";
|
||||
import {
|
||||
import {
|
||||
SiNixos,
|
||||
SiKubernetes
|
||||
} from "react-icons/si";
|
||||
@@ -150,33 +150,33 @@ export default function Page() {
|
||||
const commands = {
|
||||
mac: {
|
||||
"Apple Silicon (arm64)": [
|
||||
`curl -fsSL https://digpangolin.com/get-olm.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
|
||||
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
],
|
||||
"Intel x64 (amd64)": [
|
||||
`curl -fsSL https://digpangolin.com/get-olm.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
|
||||
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
]
|
||||
},
|
||||
linux: {
|
||||
amd64: [
|
||||
`curl -fsSL https://digpangolin.com/get-olm.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
|
||||
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
],
|
||||
arm64: [
|
||||
`curl -fsSL https://digpangolin.com/get-olm.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
|
||||
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
],
|
||||
arm32: [
|
||||
`curl -fsSL https://digpangolin.com/get-olm.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
|
||||
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
],
|
||||
arm32v6: [
|
||||
`curl -fsSL https://digpangolin.com/get-olm.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
|
||||
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
],
|
||||
riscv64: [
|
||||
`curl -fsSL https://digpangolin.com/get-olm.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-olm.sh | bash`,
|
||||
`sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
]
|
||||
},
|
||||
@@ -342,14 +342,14 @@ export default function Page() {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/fosrl/olm/releases/latest`,
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
t("olmErrorFetchReleases", {
|
||||
|
||||
@@ -168,13 +168,10 @@ export default function GeneralPage() {
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p className="mb-2">
|
||||
{t("orgQuestionRemove", {
|
||||
selectedOrg: org?.org.name
|
||||
})}
|
||||
<p>
|
||||
{t("orgQuestionRemove")}
|
||||
</p>
|
||||
<p className="mb-2">{t("orgMessageRemove")}</p>
|
||||
<p>{t("orgMessageConfirm")}</p>
|
||||
<p>{t("orgMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("orgDeleteConfirm")}
|
||||
|
||||
@@ -135,7 +135,7 @@ const addTargetSchema = z
|
||||
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
priority: z.number().int().min(1).max(1000)
|
||||
priority: z.number().int().min(1).max(1000).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -205,6 +205,7 @@ export default function ReverseProxyTargets(props: {
|
||||
}) {
|
||||
const params = use(props.params);
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const { resource, updateResource } = useResourceContext();
|
||||
|
||||
@@ -428,17 +429,19 @@ export default function ReverseProxyTargets(props: {
|
||||
}, [isAdvancedMode]);
|
||||
|
||||
function addNewTarget() {
|
||||
const isHttp = resource.http;
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
targetId: -Date.now(), // Use negative timestamp as temporary ID
|
||||
ip: "",
|
||||
method: resource.http ? "http" : null,
|
||||
method: isHttp ? "http" : null,
|
||||
port: 0,
|
||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
||||
path: null,
|
||||
pathMatchType: null,
|
||||
rewritePath: null,
|
||||
rewritePathType: null,
|
||||
priority: 100,
|
||||
path: isHttp ? null : null,
|
||||
pathMatchType: isHttp ? null : null,
|
||||
rewritePath: isHttp ? null : null,
|
||||
rewritePathType: isHttp ? null : null,
|
||||
priority: isHttp ? 100 : 100,
|
||||
enabled: true,
|
||||
resourceId: resource.resourceId,
|
||||
hcEnabled: false,
|
||||
@@ -514,25 +517,31 @@ export default function ReverseProxyTargets(props: {
|
||||
try {
|
||||
setTargetsLoading(true);
|
||||
|
||||
const response = await api.post<
|
||||
AxiosResponse<CreateTargetResponse>
|
||||
>(`/target`, {
|
||||
const data: any = {
|
||||
resourceId: resource.resourceId,
|
||||
siteId: target.siteId,
|
||||
ip: target.ip,
|
||||
method: target.method,
|
||||
port: target.port,
|
||||
path: target.path,
|
||||
pathMatchType: target.pathMatchType,
|
||||
rewritePath: target.rewritePath,
|
||||
rewritePathType: target.rewritePathType,
|
||||
priority: target.priority,
|
||||
enabled: target.enabled,
|
||||
hcEnabled: target.hcEnabled,
|
||||
hcPath: target.hcPath,
|
||||
hcInterval: target.hcInterval,
|
||||
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) {
|
||||
// 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 isHttp = resource.http;
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
...data,
|
||||
path: data.path || null,
|
||||
pathMatchType: data.pathMatchType || null,
|
||||
rewritePath: data.rewritePath || null,
|
||||
rewritePathType: data.rewritePathType || null,
|
||||
path: isHttp ? (data.path || null) : null,
|
||||
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
|
||||
rewritePath: isHttp ? (data.rewritePath || null) : null,
|
||||
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
|
||||
siteType: site?.type || null,
|
||||
enabled: true,
|
||||
targetId: new Date().getTime(),
|
||||
new: true,
|
||||
resourceId: resource.resourceId,
|
||||
priority: 100,
|
||||
priority: isHttp ? (data.priority || 100) : 100,
|
||||
hcEnabled: false,
|
||||
hcPath: null,
|
||||
hcMethod: null,
|
||||
@@ -666,7 +676,7 @@ export default function ReverseProxyTargets(props: {
|
||||
...target,
|
||||
...data,
|
||||
updated: true,
|
||||
// siteType: site?.type || null
|
||||
siteType: site ? site.type : target.siteType
|
||||
}
|
||||
: target
|
||||
)
|
||||
@@ -719,7 +729,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
// Save targets
|
||||
for (const target of targets) {
|
||||
const data = {
|
||||
const data: any = {
|
||||
ip: target.ip,
|
||||
port: target.port,
|
||||
method: target.method,
|
||||
@@ -735,14 +745,18 @@ export default function ReverseProxyTargets(props: {
|
||||
hcHeaders: target.hcHeaders || null,
|
||||
hcFollowRedirects: target.hcFollowRedirects || null,
|
||||
hcMethod: target.hcMethod || null,
|
||||
hcStatus: target.hcStatus || null,
|
||||
path: target.path,
|
||||
pathMatchType: target.pathMatchType,
|
||||
rewritePath: target.rewritePath,
|
||||
rewritePathType: target.rewritePathType,
|
||||
priority: target.priority
|
||||
hcStatus: target.hcStatus || null
|
||||
};
|
||||
|
||||
// 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) {
|
||||
const res = await api.put<
|
||||
AxiosResponse<CreateTargetResponse>
|
||||
@@ -814,6 +828,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
||||
const baseColumns: ColumnDef<LocalTarget>[] = [];
|
||||
const isHttp = resource.http;
|
||||
|
||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||
id: "priority",
|
||||
@@ -839,6 +854,7 @@ export default function ReverseProxyTargets(props: {
|
||||
type="number"
|
||||
min="1"
|
||||
max="1000"
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
defaultValue={row.original.priority || 100}
|
||||
className="w-full max-w-20"
|
||||
onBlur={(e) => {
|
||||
@@ -1007,14 +1023,9 @@ export default function ReverseProxyTargets(props: {
|
||||
) => {
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
ip: hostname
|
||||
ip: hostname,
|
||||
...(port && { port: port })
|
||||
});
|
||||
if (port) {
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
port: port
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -1051,12 +1062,12 @@ export default function ReverseProxyTargets(props: {
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
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 &&
|
||||
"text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate max-w-[90px]">
|
||||
<span className="truncate max-w-[150px]">
|
||||
{row.original.siteId
|
||||
? selectedSite?.name
|
||||
: t("siteSelect")}
|
||||
@@ -1133,7 +1144,7 @@ export default function ReverseProxyTargets(props: {
|
||||
<Input
|
||||
defaultValue={row.original.ip}
|
||||
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) => {
|
||||
const input = e.target.value.trim();
|
||||
const hasProtocol =
|
||||
@@ -1317,15 +1328,20 @@ export default function ReverseProxyTargets(props: {
|
||||
};
|
||||
|
||||
if (isAdvancedMode) {
|
||||
return [
|
||||
matchPathColumn,
|
||||
const columns = [
|
||||
addressColumn,
|
||||
rewritePathColumn,
|
||||
priorityColumn,
|
||||
healthCheckColumn,
|
||||
enabledColumn,
|
||||
actionsColumn
|
||||
];
|
||||
|
||||
// Only include path-related columns for HTTP resources
|
||||
if (isHttp) {
|
||||
columns.unshift(matchPathColumn);
|
||||
columns.splice(3, 0, rewritePathColumn, priorityColumn);
|
||||
}
|
||||
|
||||
return columns;
|
||||
} else {
|
||||
return [
|
||||
addressColumn,
|
||||
@@ -1454,7 +1470,7 @@ export default function ReverseProxyTargets(props: {
|
||||
/>
|
||||
<label
|
||||
htmlFor="advanced-mode-toggle"
|
||||
className="text-sm font-medium"
|
||||
className="text-sm"
|
||||
>
|
||||
{t("advancedMode")}
|
||||
</label>
|
||||
@@ -1496,7 +1512,7 @@ export default function ReverseProxyTargets(props: {
|
||||
className="space-y-4"
|
||||
id="tls-settings-form"
|
||||
>
|
||||
{build == "oss" && (
|
||||
{!env.flags.usePangolinDns && (
|
||||
<FormField
|
||||
control={tlsSettingsForm.control}
|
||||
name="ssl"
|
||||
|
||||
@@ -438,6 +438,7 @@ export default function ResourceRules(props: {
|
||||
defaultValue={row.original.priority}
|
||||
className="w-[75px]"
|
||||
type="number"
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onBlur={(e) => {
|
||||
const parsed = z.coerce
|
||||
.number()
|
||||
|
||||
@@ -25,7 +25,6 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Checkbox } from "@app/components/ui/checkbox";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { ListSitesResponse } from "@server/routers/site";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
@@ -58,7 +57,16 @@ import {
|
||||
} from "@app/components/ui/popover";
|
||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
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 Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -89,16 +97,25 @@ import { isTargetValid } from "@server/lib/validators";
|
||||
import { ListTargetsResponse } from "@server/routers/target";
|
||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
||||
import { toASCII, toUnicode } from 'punycode';
|
||||
import { toASCII, toUnicode } from "punycode";
|
||||
import { DomainRow } from "../../../../../components/DomainsTable";
|
||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
||||
import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal";
|
||||
import {
|
||||
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 HealthCheckDialog from "@app/components/HealthCheckDialog";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
|
||||
|
||||
const baseResourceFormSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
http: z.boolean()
|
||||
@@ -115,54 +132,57 @@ const tcpUdpResourceFormSchema = z.object({
|
||||
// enableProxy: z.boolean().default(false)
|
||||
});
|
||||
|
||||
const targetsSettingsSchema = z.object({
|
||||
stickySession: z.boolean()
|
||||
});
|
||||
|
||||
|
||||
const addTargetSchema = z.object({
|
||||
ip: z.string().refine(isTargetValid),
|
||||
method: z.string().nullable(),
|
||||
port: z.coerce.number().int().positive(),
|
||||
siteId: z.number().int().positive(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(),
|
||||
rewritePath: z.string().optional().nullable(),
|
||||
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(),
|
||||
priority: z.number().int().min(1).max(1000)
|
||||
}).refine(
|
||||
(data) => {
|
||||
// If path is provided, pathMatchType must be provided
|
||||
if (data.path && !data.pathMatchType) {
|
||||
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;
|
||||
}
|
||||
const addTargetSchema = z
|
||||
.object({
|
||||
ip: z.string().refine(isTargetValid),
|
||||
method: z.string().nullable(),
|
||||
port: z.coerce.number().int().positive(),
|
||||
siteId: z.number().int().positive(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z
|
||||
.enum(["exact", "prefix", "regex"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
rewritePath: z.string().optional().nullable(),
|
||||
rewritePathType: z
|
||||
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
priority: z.number().int().min(1).max(1000).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// If path is provided, pathMatchType must be provided
|
||||
if (data.path && !data.pathMatchType) {
|
||||
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(
|
||||
(data) => {
|
||||
// If rewritePath is provided, rewritePathType must be provided
|
||||
@@ -216,12 +236,14 @@ export default function Page() {
|
||||
>([]);
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [showSnippets, setShowSnippets] = useState(false);
|
||||
const [resourceId, setResourceId] = useState<number | null>(null);
|
||||
const [niceId, setNiceId] = useState<string>("");
|
||||
|
||||
// Target management state
|
||||
const [targets, setTargets] = useState<LocalTarget[]>([]);
|
||||
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] =
|
||||
useState<LocalTarget | null>(null);
|
||||
@@ -246,17 +268,19 @@ export default function Page() {
|
||||
}, [isAdvancedMode]);
|
||||
|
||||
function addNewTarget() {
|
||||
const isHttp = baseForm.watch("http");
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
targetId: -Date.now(), // Use negative timestamp as temporary ID
|
||||
ip: "",
|
||||
method: baseForm.watch("http") ? "http" : null,
|
||||
method: isHttp ? "http" : null,
|
||||
port: 0,
|
||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
||||
path: null,
|
||||
pathMatchType: null,
|
||||
rewritePath: null,
|
||||
rewritePathType: null,
|
||||
priority: 100,
|
||||
path: isHttp ? null : null,
|
||||
pathMatchType: isHttp ? null : null,
|
||||
rewritePath: isHttp ? null : null,
|
||||
rewritePathType: isHttp ? null : null,
|
||||
priority: isHttp ? 100 : 100,
|
||||
enabled: true,
|
||||
resourceId: 0,
|
||||
hcEnabled: false,
|
||||
@@ -290,12 +314,12 @@ export default function Page() {
|
||||
...(!env.flags.allowRawResources
|
||||
? []
|
||||
: [
|
||||
{
|
||||
id: "raw" as ResourceType,
|
||||
title: t("resourceRaw"),
|
||||
description: t("resourceRawDescription")
|
||||
}
|
||||
])
|
||||
{
|
||||
id: "raw" as ResourceType,
|
||||
title: t("resourceRaw"),
|
||||
description: t("resourceRawDescription")
|
||||
}
|
||||
])
|
||||
];
|
||||
|
||||
const baseForm = useForm({
|
||||
@@ -330,26 +354,39 @@ export default function Page() {
|
||||
pathMatchType: null,
|
||||
rewritePath: null,
|
||||
rewritePathType: null,
|
||||
priority: 100,
|
||||
priority: baseForm.watch("http") ? 100 : undefined
|
||||
} as z.infer<typeof addTargetSchema>
|
||||
});
|
||||
|
||||
const targetsSettingsForm = useForm({
|
||||
resolver: zodResolver(targetsSettingsSchema),
|
||||
defaultValues: {
|
||||
stickySession: false
|
||||
}
|
||||
});
|
||||
// Helper function to check if all targets have required fields using schema validation
|
||||
const areAllTargetsValid = () => {
|
||||
if (targets.length === 0) return true; // No targets is valid
|
||||
|
||||
const watchedIp = addTargetForm.watch("ip");
|
||||
const watchedPort = addTargetForm.watch("port");
|
||||
const watchedSiteId = addTargetForm.watch("siteId");
|
||||
return targets.every((target) => {
|
||||
try {
|
||||
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) => {
|
||||
addTargetForm.setValue("ip", hostname);
|
||||
if (port) {
|
||||
addTargetForm.setValue("port", port);
|
||||
}
|
||||
// Only include priority for HTTP resources
|
||||
if (isHttp) {
|
||||
targetData.priority = target.priority;
|
||||
}
|
||||
|
||||
addTargetSchema.parse(targetData);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const initializeDockerForSite = async (siteId: number) => {
|
||||
@@ -360,14 +397,14 @@ export default function Page() {
|
||||
const dockerManager = new DockerManager(api, siteId);
|
||||
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 dockerManager = new DockerManager(api, siteId);
|
||||
const containers = await dockerManager.fetchContainers();
|
||||
|
||||
setDockerStates(prev => {
|
||||
setDockerStates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
const existingState = newMap.get(siteId);
|
||||
if (existingState) {
|
||||
@@ -378,11 +415,13 @@ export default function Page() {
|
||||
};
|
||||
|
||||
const getDockerStateForSite = (siteId: number): DockerState => {
|
||||
return dockerStates.get(siteId) || {
|
||||
isEnabled: false,
|
||||
isAvailable: false,
|
||||
containers: []
|
||||
};
|
||||
return (
|
||||
dockerStates.get(siteId) || {
|
||||
isEnabled: false,
|
||||
isAvailable: false,
|
||||
containers: []
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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 isHttp = baseForm.watch("http");
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
...data,
|
||||
path: data.path || null,
|
||||
pathMatchType: data.pathMatchType || null,
|
||||
rewritePath: data.rewritePath || null,
|
||||
rewritePathType: data.rewritePathType || null,
|
||||
path: isHttp ? (data.path || null) : null,
|
||||
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
|
||||
rewritePath: isHttp ? (data.rewritePath || null) : null,
|
||||
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
|
||||
siteType: site?.type || null,
|
||||
enabled: true,
|
||||
targetId: new Date().getTime(),
|
||||
new: true,
|
||||
resourceId: 0, // Will be set when resource is created
|
||||
priority: 100, // Default priority
|
||||
priority: isHttp ? (data.priority || 100) : 100, // Default priority
|
||||
hcEnabled: false,
|
||||
hcPath: null,
|
||||
hcMethod: null,
|
||||
@@ -443,7 +484,7 @@ export default function Page() {
|
||||
pathMatchType: null,
|
||||
rewritePath: null,
|
||||
rewritePathType: null,
|
||||
priority: 100,
|
||||
priority: isHttp ? 100 : undefined
|
||||
});
|
||||
}
|
||||
|
||||
@@ -463,11 +504,11 @@ export default function Page() {
|
||||
targets.map((target) =>
|
||||
target.targetId === targetId
|
||||
? {
|
||||
...target,
|
||||
...data,
|
||||
updated: true,
|
||||
siteType: site?.type || null
|
||||
}
|
||||
...target,
|
||||
...data,
|
||||
updated: true,
|
||||
siteType: site ? site.type : target.siteType
|
||||
}
|
||||
: target
|
||||
)
|
||||
);
|
||||
@@ -478,13 +519,11 @@ export default function Page() {
|
||||
|
||||
const baseData = baseForm.getValues();
|
||||
const isHttp = baseData.http;
|
||||
const stickySessionData = targetsSettingsForm.getValues();
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
name: baseData.name,
|
||||
http: baseData.http,
|
||||
stickySession: stickySessionData.stickySession
|
||||
};
|
||||
|
||||
let sanitizedSubdomain: string | undefined;
|
||||
@@ -497,7 +536,9 @@ export default function Page() {
|
||||
: undefined;
|
||||
|
||||
Object.assign(payload, {
|
||||
subdomain: sanitizedSubdomain ? toASCII(sanitizedSubdomain) : undefined,
|
||||
subdomain: sanitizedSubdomain
|
||||
? toASCII(sanitizedSubdomain)
|
||||
: undefined,
|
||||
domainId: httpData.domainId,
|
||||
protocol: "tcp"
|
||||
});
|
||||
@@ -528,13 +569,13 @@ export default function Page() {
|
||||
if (res && res.status === 201) {
|
||||
const id = res.data.data.resourceId;
|
||||
const niceId = res.data.data.niceId;
|
||||
setResourceId(id);
|
||||
setNiceId(niceId);
|
||||
|
||||
// Create targets if any exist
|
||||
if (targets.length > 0) {
|
||||
try {
|
||||
for (const target of targets) {
|
||||
const data = {
|
||||
const data: any = {
|
||||
ip: target.ip,
|
||||
port: target.port,
|
||||
method: target.method,
|
||||
@@ -551,14 +592,18 @@ export default function Page() {
|
||||
hcPort: target.hcPort || null,
|
||||
hcFollowRedirects:
|
||||
target.hcFollowRedirects || null,
|
||||
hcStatus: target.hcStatus || null,
|
||||
path: target.path,
|
||||
pathMatchType: target.pathMatchType,
|
||||
rewritePath: target.rewritePath,
|
||||
rewritePathType: target.rewritePathType,
|
||||
priority: target.priority
|
||||
hcStatus: target.hcStatus || null
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
} catch (targetError) {
|
||||
@@ -660,7 +705,7 @@ export default function Page() {
|
||||
const rawDomains = res.data.data.domains as DomainRow[];
|
||||
const domains = rawDomains.map((domain) => ({
|
||||
...domain,
|
||||
baseDomain: toUnicode(domain.baseDomain),
|
||||
baseDomain: toUnicode(domain.baseDomain)
|
||||
}));
|
||||
setBaseDomains(domains);
|
||||
// if (domains.length) {
|
||||
@@ -683,10 +728,10 @@ export default function Page() {
|
||||
targets.map((target) =>
|
||||
target.targetId === targetId
|
||||
? {
|
||||
...target,
|
||||
...config,
|
||||
updated: true
|
||||
}
|
||||
...target,
|
||||
...config,
|
||||
updated: true
|
||||
}
|
||||
: target
|
||||
)
|
||||
);
|
||||
@@ -700,6 +745,7 @@ export default function Page() {
|
||||
|
||||
const getColumns = (): ColumnDef<LocalTarget>[] => {
|
||||
const baseColumns: ColumnDef<LocalTarget>[] = [];
|
||||
const isHttp = baseForm.watch("http");
|
||||
|
||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||
id: "priority",
|
||||
@@ -712,9 +758,7 @@ export default function Page() {
|
||||
<Info className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<p>
|
||||
{t("priorityDescription")}
|
||||
</p>
|
||||
<p>{t("priorityDescription")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -895,31 +939,51 @@ export default function Page() {
|
||||
) => {
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
ip: hostname
|
||||
ip: hostname,
|
||||
...(port && { port: port })
|
||||
});
|
||||
if (port) {
|
||||
updateTarget(row.original.targetId, {
|
||||
...row.original,
|
||||
port: port
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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">
|
||||
{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>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
role="combobox"
|
||||
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 &&
|
||||
"text-muted-foreground"
|
||||
"text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate max-w-[90px]">
|
||||
<span className="truncate max-w-[150px]">
|
||||
{row.original.siteId
|
||||
? selectedSite?.name
|
||||
: t("siteSelect")}
|
||||
@@ -969,30 +1033,6 @@ export default function Page() {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</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
|
||||
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"}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -1020,7 +1060,7 @@ export default function Page() {
|
||||
<Input
|
||||
defaultValue={row.original.ip}
|
||||
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) => {
|
||||
const input = e.target.value.trim();
|
||||
const hasProtocol =
|
||||
@@ -1204,15 +1244,20 @@ export default function Page() {
|
||||
};
|
||||
|
||||
if (isAdvancedMode) {
|
||||
return [
|
||||
matchPathColumn,
|
||||
const columns = [
|
||||
addressColumn,
|
||||
rewritePathColumn,
|
||||
priorityColumn,
|
||||
healthCheckColumn,
|
||||
enabledColumn,
|
||||
actionsColumn
|
||||
];
|
||||
|
||||
// Only include path-related columns for HTTP resources
|
||||
if (isHttp) {
|
||||
columns.unshift(matchPathColumn);
|
||||
columns.splice(3, 0, rewritePathColumn, priorityColumn);
|
||||
}
|
||||
|
||||
return columns;
|
||||
} else {
|
||||
return [
|
||||
addressColumn,
|
||||
@@ -1464,10 +1509,10 @@ export default function Page() {
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
@@ -1546,60 +1591,87 @@ export default function Page() {
|
||||
<TableHeader>
|
||||
{table
|
||||
.getHeaderGroups()
|
||||
.map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map(
|
||||
(header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table
|
||||
.getRowModel()
|
||||
.rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => (
|
||||
<TableCell
|
||||
.map(
|
||||
(
|
||||
headerGroup
|
||||
) => (
|
||||
<TableRow
|
||||
key={
|
||||
headerGroup.id
|
||||
}
|
||||
>
|
||||
{headerGroup.headers.map(
|
||||
(
|
||||
header
|
||||
) => (
|
||||
<TableHead
|
||||
key={
|
||||
cell.id
|
||||
header.id
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell
|
||||
.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
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={
|
||||
cell.id
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell
|
||||
.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
)
|
||||
)}
|
||||
</TableRow>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
colSpan={
|
||||
columns.length
|
||||
}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{t("targetNoOne")}
|
||||
{t(
|
||||
"targetNoOne"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -1621,12 +1693,16 @@ export default function Page() {
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="advanced-mode-toggle"
|
||||
checked={isAdvancedMode}
|
||||
onCheckedChange={setIsAdvancedMode}
|
||||
checked={
|
||||
isAdvancedMode
|
||||
}
|
||||
onCheckedChange={
|
||||
setIsAdvancedMode
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="advanced-mode-toggle"
|
||||
className="text-sm font-medium"
|
||||
className="text-sm"
|
||||
>
|
||||
{t("advancedMode")}
|
||||
</label>
|
||||
@@ -1639,7 +1715,10 @@ export default function Page() {
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{t("targetNoOne")}
|
||||
</p>
|
||||
<Button onClick={addNewTarget} variant="outline">
|
||||
<Button
|
||||
onClick={addNewTarget}
|
||||
variant="outline"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t("addTarget")}
|
||||
</Button>
|
||||
@@ -1677,6 +1756,7 @@ export default function Page() {
|
||||
}
|
||||
}}
|
||||
loading={createLoading}
|
||||
disabled={!areAllTargetsValid()}
|
||||
>
|
||||
{t("resourceCreate")}
|
||||
</Button>
|
||||
@@ -1685,24 +1765,36 @@ export default function Page() {
|
||||
<HealthCheckDialog
|
||||
open={healthCheckDialogOpen}
|
||||
setOpen={setHealthCheckDialogOpen}
|
||||
targetId={selectedTargetForHealthCheck.targetId}
|
||||
targetId={
|
||||
selectedTargetForHealthCheck.targetId
|
||||
}
|
||||
targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`}
|
||||
targetMethod={
|
||||
selectedTargetForHealthCheck.method || undefined
|
||||
selectedTargetForHealthCheck.method ||
|
||||
undefined
|
||||
}
|
||||
initialConfig={{
|
||||
hcEnabled:
|
||||
selectedTargetForHealthCheck.hcEnabled || false,
|
||||
hcPath: selectedTargetForHealthCheck.hcPath || "/",
|
||||
selectedTargetForHealthCheck.hcEnabled ||
|
||||
false,
|
||||
hcPath:
|
||||
selectedTargetForHealthCheck.hcPath ||
|
||||
"/",
|
||||
hcMethod:
|
||||
selectedTargetForHealthCheck.hcMethod || "GET",
|
||||
selectedTargetForHealthCheck.hcMethod ||
|
||||
"GET",
|
||||
hcInterval:
|
||||
selectedTargetForHealthCheck.hcInterval || 5,
|
||||
hcTimeout: selectedTargetForHealthCheck.hcTimeout || 5,
|
||||
selectedTargetForHealthCheck.hcInterval ||
|
||||
5,
|
||||
hcTimeout:
|
||||
selectedTargetForHealthCheck.hcTimeout ||
|
||||
5,
|
||||
hcHeaders:
|
||||
selectedTargetForHealthCheck.hcHeaders || undefined,
|
||||
selectedTargetForHealthCheck.hcHeaders ||
|
||||
undefined,
|
||||
hcScheme:
|
||||
selectedTargetForHealthCheck.hcScheme || undefined,
|
||||
selectedTargetForHealthCheck.hcScheme ||
|
||||
undefined,
|
||||
hcHostname:
|
||||
selectedTargetForHealthCheck.hcHostname ||
|
||||
selectedTargetForHealthCheck.ip,
|
||||
@@ -1713,8 +1805,11 @@ export default function Page() {
|
||||
selectedTargetForHealthCheck.hcFollowRedirects ||
|
||||
true,
|
||||
hcStatus:
|
||||
selectedTargetForHealthCheck.hcStatus || undefined,
|
||||
hcMode: selectedTargetForHealthCheck.hcMode || "http",
|
||||
selectedTargetForHealthCheck.hcStatus ||
|
||||
undefined,
|
||||
hcMode:
|
||||
selectedTargetForHealthCheck.hcMode ||
|
||||
"http",
|
||||
hcUnhealthyInterval:
|
||||
selectedTargetForHealthCheck.hcUnhealthyInterval ||
|
||||
30
|
||||
@@ -1749,7 +1844,9 @@ export default function Page() {
|
||||
{t("resourceAddEntrypoints")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("resourceAddEntrypointsEditFile")}
|
||||
{t(
|
||||
"resourceAddEntrypointsEditFile"
|
||||
)}
|
||||
</p>
|
||||
<CopyTextBox
|
||||
text={`entryPoints:
|
||||
@@ -1764,7 +1861,9 @@ export default function Page() {
|
||||
{t("resourceExposePorts")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("resourceExposePortsEditFile")}
|
||||
{t(
|
||||
"resourceExposePortsEditFile"
|
||||
)}
|
||||
</p>
|
||||
<CopyTextBox
|
||||
text={`ports:
|
||||
@@ -1775,7 +1874,7 @@ export default function Page() {
|
||||
|
||||
<Link
|
||||
className="text-sm text-primary flex items-center gap-1"
|
||||
href="https://docs.digpangolin.com/manage/resources/tcp-udp-resources"
|
||||
href="https://docs.pangolin.net/manage/resources/tcp-udp-resources"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -1802,7 +1901,7 @@ export default function Page() {
|
||||
type="button"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/${orgId}/settings/resources/${resourceId}/proxy`
|
||||
`/${orgId}/settings/resources/${niceId}/proxy`
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -43,7 +43,7 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||
await authCookieHeader()
|
||||
);
|
||||
resources = res.data.data.resources;
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
|
||||
let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = [];
|
||||
try {
|
||||
@@ -51,7 +51,7 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||
AxiosResponse<ListAllSiteResourcesByOrgResponse>
|
||||
>(`/org/${params.orgId}/site-resources`, await authCookieHeader());
|
||||
siteResources = res.data.data.siteResources;
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
|
||||
let org = null;
|
||||
try {
|
||||
@@ -88,8 +88,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||
resource.passwordId !== null ||
|
||||
resource.whitelist ||
|
||||
resource.headerAuthId
|
||||
? "protected"
|
||||
: "not_protected",
|
||||
? "protected"
|
||||
: "not_protected",
|
||||
enabled: resource.enabled,
|
||||
domainId: resource.domainId || undefined,
|
||||
ssl: resource.ssl
|
||||
@@ -128,6 +128,10 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||
defaultView={
|
||||
env.flags.enableClients ? defaultView : "proxy"
|
||||
}
|
||||
defaultSort={{
|
||||
id: "name",
|
||||
desc: false
|
||||
}}
|
||||
/>
|
||||
</OrgProvider>
|
||||
</>
|
||||
|
||||
@@ -238,7 +238,7 @@ export default function GeneralPage() {
|
||||
"enableDockerSocketDescription"
|
||||
)}{" "}
|
||||
<Link
|
||||
href="https://docs.digpangolin.com/manage/sites/configure-site#docker-socket-integration"
|
||||
href="https://docs.pangolin.net/manage/sites/configure-site#docker-socket-integration"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline inline-flex items-center"
|
||||
|
||||
@@ -252,43 +252,43 @@ PersistentKeepalive = 5`;
|
||||
const commands = {
|
||||
mac: {
|
||||
All: [
|
||||
`curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
`newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
// "Intel x64 (amd64)": [
|
||||
// `curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
// ]
|
||||
},
|
||||
linux: {
|
||||
All: [
|
||||
`curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
`newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
// arm64: [
|
||||
// `curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
// ],
|
||||
// arm32: [
|
||||
// `curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
// ],
|
||||
// arm32v6: [
|
||||
// `curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
// ],
|
||||
// riscv64: [
|
||||
// `curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
// ]
|
||||
},
|
||||
freebsd: {
|
||||
All: [
|
||||
`curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
`curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
`newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
// arm64: [
|
||||
// `curl -fsSL https://digpangolin.com/get-newt.sh | bash`,
|
||||
// `curl -fsSL https://pangolin.net/get-newt.sh | bash`,
|
||||
// `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
// ]
|
||||
},
|
||||
|
||||
@@ -326,7 +326,7 @@ export default function PoliciesPage() {
|
||||
{/*TODO(vlalx): Validate replacing */}
|
||||
{t('orgPoliciesAboutDescription')}{" "}
|
||||
<Link
|
||||
href="https://docs.digpangolin.com/manage/identity-providers/auto-provisioning"
|
||||
href="https://docs.pangolin.net/manage/identity-providers/auto-provisioning"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
|
||||
@@ -315,18 +315,13 @@ export default function LicensePage() {
|
||||
setSelectedLicenseKey(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("licenseQuestionRemove", {
|
||||
selectedKey: obfuscateLicenseKey(
|
||||
selectedLicenseKey.licenseKey
|
||||
)
|
||||
})}
|
||||
{t("licenseQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
<b>{t("licenseMessageRemove")}</b>
|
||||
</p>
|
||||
<p>{t("licenseMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("licenseKeyDeleteConfirm")}
|
||||
|
||||
@@ -237,21 +237,14 @@ export default function UsersTable({ users }: Props) {
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("userQuestionRemove", {
|
||||
selectedUser:
|
||||
selected?.email ||
|
||||
selected?.name ||
|
||||
selected?.username
|
||||
})}
|
||||
{t("userQuestionRemove")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>{t("userMessageRemove")}</b>
|
||||
{t("userMessageRemove")}
|
||||
</p>
|
||||
|
||||
<p>{t("userMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("userDeleteConfirm")}
|
||||
|
||||
@@ -147,7 +147,7 @@ export default async function OrgAuthPage(props: {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
<Link
|
||||
href="https://digpangolin.com/"
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
|
||||
@@ -67,6 +67,12 @@ export default async function RootLayout({
|
||||
)
|
||||
)();
|
||||
licenseStatus = licenseStatusRes.data.data;
|
||||
} else if (build === "saas") {
|
||||
licenseStatus = {
|
||||
isHostLicensed: true,
|
||||
isLicenseValid: true,
|
||||
hostId: "saas"
|
||||
};
|
||||
} else {
|
||||
licenseStatus = {
|
||||
isHostLicensed: false,
|
||||
|
||||
@@ -193,7 +193,7 @@ export default function IdpTable({ idps }: Props) {
|
||||
setSelectedIdp(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("idpQuestionRemove", {
|
||||
name: selectedIdp.name
|
||||
|
||||
@@ -256,7 +256,7 @@ export default function UsersTable({ users }: Props) {
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("userQuestionRemove", {
|
||||
selectedUser:
|
||||
|
||||
@@ -177,19 +177,14 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("apiKeysQuestionRemove", {
|
||||
selectedApiKey:
|
||||
selected?.name || selected?.id
|
||||
})}
|
||||
{t("apiKeysQuestionRemove")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>{t("apiKeysMessageRemove")}</b>
|
||||
{t("apiKeysMessageRemove")}
|
||||
</p>
|
||||
|
||||
<p>{t("apiKeysMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("apiKeysDeleteConfirm")}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useTheme } from "next-themes";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -13,6 +14,7 @@ type BrandingLogoProps = {
|
||||
export default function BrandingLogo(props: BrandingLogoProps) {
|
||||
const { env } = useEnvContext();
|
||||
const { theme } = useTheme();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
const [path, setPath] = useState<string>(""); // Default logo path
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,12 +29,16 @@ export default function BrandingLogo(props: BrandingLogoProps) {
|
||||
}
|
||||
|
||||
if (lightOrDark === "light") {
|
||||
return (
|
||||
env.branding.logo?.lightPath || "/logo/word_mark_black.png"
|
||||
);
|
||||
if (isUnlocked() && env.branding.logo?.lightPath) {
|
||||
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();
|
||||
|
||||
@@ -277,25 +277,12 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||
setSelectedClient(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
Are you sure you want to remove the client{" "}
|
||||
<b>
|
||||
{selectedClient?.name || selectedClient?.id}
|
||||
</b>{" "}
|
||||
from the site and organization?
|
||||
{t("deleteClientQuestion")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>
|
||||
Once removed, the client will no longer be
|
||||
able to connect to the site.{" "}
|
||||
</b>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To confirm, please type the name of the client
|
||||
below.
|
||||
{t("clientMessageRemove")}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { Description } from "@radix-ui/react-toast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CopyToClipboard from "./CopyToClipboard";
|
||||
|
||||
type InviteUserFormProps = {
|
||||
open: boolean;
|
||||
@@ -110,6 +111,17 @@ export default function InviteUserForm({
|
||||
<CredenzaBody>
|
||||
<div className="mb-4 break-all overflow-hidden">
|
||||
{dialog}
|
||||
<div className="mt-2 mb-6 font-bold text-red-700">
|
||||
{t("cannotbeUndone")}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
{t("type")}
|
||||
<span className="px-2 py-1 rounded-md bg-secondary"><CopyToClipboard text={string} /></span>
|
||||
{t("toConfirm")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -16,6 +16,7 @@ import Image from "next/image";
|
||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import BrandingLogo from "@app/components/BrandingLogo";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
|
||||
type DashboardLoginFormProps = {
|
||||
redirect?: string;
|
||||
@@ -29,18 +30,22 @@ export default function DashboardLoginForm({
|
||||
const router = useRouter();
|
||||
const { env } = useEnvContext();
|
||||
const t = useTranslations();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
function getSubtitle() {
|
||||
return t("loginStart");
|
||||
}
|
||||
|
||||
const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 175 : 175;
|
||||
const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 58 : 58;
|
||||
|
||||
return (
|
||||
<Card className="shadow-md w-full max-w-md">
|
||||
<CardHeader className="border-b">
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<BrandingLogo
|
||||
height={env.branding.logo?.authPage?.height || 58}
|
||||
width={env.branding.logo?.authPage?.width || 175}
|
||||
height={logoHeight}
|
||||
width={logoWidth}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-center space-y-1 pt-3">
|
||||
|
||||
@@ -237,16 +237,13 @@ export default function DomainsTable({ domains }: Props) {
|
||||
setSelectedDomain(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("domainQuestionRemove", {
|
||||
domain: selectedDomain.baseDomain
|
||||
})}
|
||||
{t("domainQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
<b>{t("domainMessageRemove")}</b>
|
||||
{t("domainMessageRemove")}
|
||||
</p>
|
||||
<p>{t("domainMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("domainConfirmDelete")}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -175,14 +175,11 @@ export default function InvitationsTable({
|
||||
setSelectedInvitation(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("inviteQuestionRemove", {
|
||||
email: selectedInvitation?.email || ""
|
||||
})}
|
||||
{t("inviteQuestionRemove")}
|
||||
</p>
|
||||
<p>{t("inviteMessageRemove")}</p>
|
||||
<p>{t("inviteMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("inviteRemoveConfirm")}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import ProfileIcon from "@app/components/ProfileIcon";
|
||||
import ThemeSwitcher from "@app/components/ThemeSwitcher";
|
||||
import { useTheme } from "next-themes";
|
||||
import BrandingLogo from "./BrandingLogo";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { build } from "@server/build";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
|
||||
interface LayoutHeaderProps {
|
||||
showTopBar: boolean;
|
||||
@@ -19,6 +17,14 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
|
||||
const { theme } = useTheme();
|
||||
const [path, setPath] = useState<string>("");
|
||||
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(() => {
|
||||
function getPath() {
|
||||
@@ -50,12 +56,8 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/" className="flex items-center">
|
||||
<BrandingLogo
|
||||
width={
|
||||
env.branding.logo?.navbar?.width || 98
|
||||
}
|
||||
height={
|
||||
env.branding.logo?.navbar?.height || 32
|
||||
}
|
||||
width={logoWidth}
|
||||
height={logoHeight}
|
||||
/>
|
||||
</Link>
|
||||
{/* {build === "saas" && (
|
||||
|
||||
@@ -67,6 +67,9 @@ export function LayoutSidebar({
|
||||
}, [isSidebarCollapsed]);
|
||||
|
||||
function loadFooterLinks(): { text: string; href?: string }[] | undefined {
|
||||
if (!isUnlocked()) {
|
||||
return undefined;
|
||||
}
|
||||
if (env.branding.footer) {
|
||||
try {
|
||||
return JSON.parse(env.branding.footer);
|
||||
|
||||
@@ -185,19 +185,14 @@ export default function OrgApiKeysTable({
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p>
|
||||
{t("apiKeysQuestionRemove", {
|
||||
selectedApiKey:
|
||||
selected?.name || selected?.id
|
||||
})}
|
||||
{t("apiKeysQuestionRemove")}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<b>{t("apiKeysMessageRemove")}</b>
|
||||
{t("apiKeysMessageRemove")}
|
||||
</p>
|
||||
|
||||
<p>{t("apiKeysMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("apiKeysDeleteConfirm")}
|
||||
|
||||
@@ -48,6 +48,7 @@ import BrandingLogo from "@app/components/BrandingLogo";
|
||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
|
||||
const pinSchema = z.object({
|
||||
pin: z
|
||||
@@ -92,6 +93,7 @@ type ResourceAuthPortalProps = {
|
||||
export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const getNumMethods = () => {
|
||||
let colLength = 0;
|
||||
@@ -308,14 +310,22 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
}
|
||||
|
||||
function getTitle() {
|
||||
if (build !== "oss" && env.branding.resourceAuthPage?.titleText) {
|
||||
if (
|
||||
isUnlocked() &&
|
||||
build !== "oss" &&
|
||||
env.branding.resourceAuthPage?.titleText
|
||||
) {
|
||||
return env.branding.resourceAuthPage.titleText;
|
||||
}
|
||||
return t("authenticationRequired");
|
||||
}
|
||||
|
||||
function getSubtitle(resourceName: string) {
|
||||
if (build !== "oss" && env.branding.resourceAuthPage?.subtitleText) {
|
||||
if (
|
||||
isUnlocked() &&
|
||||
build !== "oss" &&
|
||||
env.branding.resourceAuthPage?.subtitleText
|
||||
) {
|
||||
return env.branding.resourceAuthPage.subtitleText
|
||||
.split("{{resourceName}}")
|
||||
.join(resourceName);
|
||||
@@ -325,17 +335,20 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
: 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 (
|
||||
<div>
|
||||
{!accessDenied ? (
|
||||
<div>
|
||||
{build === "enterprise" ? (
|
||||
{isUnlocked() && build === "enterprise" ? (
|
||||
!env.branding.resourceAuthPage?.hidePoweredBy && (
|
||||
<div className="text-center mb-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
<Link
|
||||
href="https://digpangolin.com/"
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
@@ -350,7 +363,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t("poweredBy")}{" "}
|
||||
<Link
|
||||
href="https://digpangolin.com/"
|
||||
href="https://pangolin.net/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
@@ -362,18 +375,13 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
)}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{build !== "oss" &&
|
||||
{isUnlocked() &&
|
||||
build !== "oss" &&
|
||||
env.branding?.resourceAuthPage?.showLogo && (
|
||||
<div className="flex flex-row items-center justify-center mb-3">
|
||||
<BrandingLogo
|
||||
height={
|
||||
env.branding.logo?.authPage
|
||||
?.height || 100
|
||||
}
|
||||
width={
|
||||
env.branding.logo?.authPage
|
||||
?.width || 100
|
||||
}
|
||||
height={logoHeight}
|
||||
width={logoWidth}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -100,6 +100,10 @@ type ResourcesTableProps = {
|
||||
internalResources: InternalResourceRow[];
|
||||
orgId: string;
|
||||
defaultView?: "proxy" | "internal";
|
||||
defaultSort?: {
|
||||
id: string;
|
||||
desc: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -143,7 +147,8 @@ export default function ResourcesTable({
|
||||
resources,
|
||||
internalResources,
|
||||
orgId,
|
||||
defaultView = "proxy"
|
||||
defaultView = "proxy",
|
||||
defaultSort
|
||||
}: ResourcesTableProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -171,12 +176,16 @@ export default function ResourcesTable({
|
||||
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
||||
const [sites, setSites] = useState<Site[]>([]);
|
||||
|
||||
const [proxySorting, setProxySorting] = useState<SortingState>([]);
|
||||
const [proxySorting, setProxySorting] = useState<SortingState>(
|
||||
defaultSort ? [defaultSort] : []
|
||||
);
|
||||
const [proxyColumnFilters, setProxyColumnFilters] =
|
||||
useState<ColumnFiltersState>([]);
|
||||
const [proxyGlobalFilter, setProxyGlobalFilter] = useState<any>([]);
|
||||
|
||||
const [internalSorting, setInternalSorting] = useState<SortingState>([]);
|
||||
const [internalSorting, setInternalSorting] = useState<SortingState>(
|
||||
defaultSort ? [defaultSort] : []
|
||||
);
|
||||
const [internalColumnFilters, setInternalColumnFilters] =
|
||||
useState<ColumnFiltersState>([]);
|
||||
const [internalGlobalFilter, setInternalGlobalFilter] = useState<any>([]);
|
||||
@@ -695,17 +704,12 @@ export default function ResourcesTable({
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p className="mb-2">
|
||||
{t("resourceQuestionRemove", {
|
||||
selectedResource:
|
||||
selectedResource?.name ||
|
||||
selectedResource?.id
|
||||
})}
|
||||
<p>
|
||||
{t("resourceQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
{t("resourceMessageRemove")}
|
||||
</p>
|
||||
|
||||
<p className="mb-2">{t("resourceMessageRemove")}</p>
|
||||
|
||||
<p>{t("resourceMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("resourceDeleteConfirm")}
|
||||
@@ -724,17 +728,12 @@ export default function ResourcesTable({
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p className="mb-2">
|
||||
{t("resourceQuestionRemove", {
|
||||
selectedResource:
|
||||
selectedInternalResource?.name ||
|
||||
selectedInternalResource?.id
|
||||
})}
|
||||
<p>
|
||||
{t("resourceQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
{t("resourceMessageRemove")}
|
||||
</p>
|
||||
|
||||
<p className="mb-2">{t("resourceMessageRemove")}</p>
|
||||
|
||||
<p>{t("resourceMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("resourceDeleteConfirm")}
|
||||
|
||||
@@ -21,6 +21,8 @@ export default function SidebarLicenseButton({
|
||||
}: SidebarLicenseButtonProps) {
|
||||
const { licenseStatus, updateLicenseStatus } = useLicenseStatusContext();
|
||||
|
||||
const url = "https://docs.pangolin.net/self-host/enterprise-edition";
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
@@ -30,21 +32,21 @@ export default function SidebarLicenseButton({
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link href="https://docs.digpangolin.com/">
|
||||
<Link href={url}>
|
||||
<Button size="icon" className="w-8 h-8">
|
||||
<TicketCheck className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={8}>
|
||||
Enable Enterprise License
|
||||
{t("sidebarEnableEnterpriseLicense")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<Link href="https://docs.digpangolin.com/">
|
||||
<Link href={url}>
|
||||
<Button size="sm" className="gap-2 w-full">
|
||||
Enable Enterprise License
|
||||
{t("sidebarEnableEnterpriseLicense")}
|
||||
</Button>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -18,9 +18,7 @@ import {
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
@@ -31,13 +29,13 @@ import { AxiosResponse } from "axios";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import Image from "next/image";
|
||||
import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import { useTranslations } from "next-intl";
|
||||
import BrandingLogo from "@app/components/BrandingLogo";
|
||||
import { build } from "@server/build";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
|
||||
// Password strength calculation
|
||||
const calculatePasswordStrength = (password: string) => {
|
||||
@@ -111,6 +109,7 @@ export default function SignupForm({
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const t = useTranslations();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
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 (
|
||||
<Card className="w-full max-w-md shadow-md">
|
||||
<CardHeader className="border-b">
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<BrandingLogo
|
||||
height={env.branding.logo?.authPage?.height || 58}
|
||||
width={env.branding.logo?.authPage?.width || 175}
|
||||
/>
|
||||
<BrandingLogo height={logoHeight} width={logoWidth} />
|
||||
</div>
|
||||
<div className="text-center space-y-1 pt-3">
|
||||
<p className="text-muted-foreground">{getSubtitle()}</p>
|
||||
@@ -509,7 +512,7 @@ export default function SignupForm({
|
||||
"signUpTerms.IAgreeToThe"
|
||||
)}{" "}
|
||||
<a
|
||||
href="https://digpangolin.com/terms-of-service.html"
|
||||
href="https://pangolin.net/terms-of-service.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
@@ -520,7 +523,7 @@ export default function SignupForm({
|
||||
</a>
|
||||
{t("signUpTerms.and")}{" "}
|
||||
<a
|
||||
href="https://digpangolin.com/privacy-policy.html"
|
||||
href="https://pangolin.net/privacy-policy.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user