Compare commits

..

1 Commits

Author SHA1 Message Date
Laurence 2e9a040174 Add global Ctrl+K command palette for navigation and search.
Provides quick access to sidebar destinations, org switching, server-filtered entity lookup, and common actions from anywhere in the authenticated layout.
2026-05-29 11:22:14 +01:00
408 changed files with 9943 additions and 30062 deletions
-1
View File
@@ -34,4 +34,3 @@ build.ts
tsconfig.json tsconfig.json
Dockerfile* Dockerfile*
drizzle.config.ts drizzle.config.ts
allowedDevOrigins.json
+4 -4
View File
@@ -77,7 +77,7 @@ jobs:
fi fi
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -149,7 +149,7 @@ jobs:
fi fi
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -204,7 +204,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -407,7 +407,7 @@ jobs:
shell: bash shell: bash
- name: Login to GitHub Container Registry (for cosign) - name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: '24' node-version: '24'
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
skopeo --version skopeo --version
- name: Install cosign - name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
- name: Input check - name: Input check
run: | run: |
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with: with:
days-before-stale: 14 days-before-stale: 14
days-before-close: 14 days-before-close: 14
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Node - name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: '24' node-version: '24'
+2 -4
View File
@@ -17,9 +17,9 @@ yarn-error.log*
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
*.db *.db
*.sqlite* *.sqlite
!Dockerfile.sqlite !Dockerfile.sqlite
*.sqlite3* *.sqlite3
*.log *.log
.machinelogs*.json .machinelogs*.json
*-audit.json *-audit.json
@@ -54,5 +54,3 @@ hydrateSaas.ts
CLAUDE.md CLAUDE.md
drizzle.config.ts drizzle.config.ts
server/setup/migrations.ts server/setup/migrations.ts
solo.yml
allowedDevOrigins.json
+1 -1
View File
@@ -107,7 +107,7 @@ the docs to illustrate some basic ideas.
## Licensing ## Licensing
Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net). 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 ## Contributions
-51
View File
@@ -1,51 +0,0 @@
import { CommandModule } from "yargs";
import { db, users } from "@server/db";
import { eq } from "drizzle-orm";
type SetServerAdminArgs = {
email: string;
};
export const setServerAdmin: CommandModule<{}, SetServerAdminArgs> = {
command: "set-server-admin",
describe: "Mark any user as a server admin by email address",
builder: (yargs) => {
return yargs.option("email", {
type: "string",
demandOption: true,
describe: "User email address"
});
},
handler: async (argv: { email: string }) => {
try {
const email = argv.email.trim().toLowerCase();
const [user] = await db
.select()
.from(users)
.where(eq(users.email, email))
.limit(1);
if (!user) {
console.error(`User with email '${email}' not found`);
process.exit(1);
}
if (user.serverAdmin) {
console.log(`User '${email}' is already a server admin`);
process.exit(0);
}
await db
.update(users)
.set({ serverAdmin: true })
.where(eq(users.userId, user.userId));
console.log(`User '${email}' has been marked as a server admin`);
process.exit(0);
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
};
-2
View File
@@ -11,7 +11,6 @@ import { deleteClient } from "./commands/deleteClient";
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys"; import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
import { clearCertificates } from "./commands/clearCertificates"; import { clearCertificates } from "./commands/clearCertificates";
import { disableUser2fa } from "./commands/disableUser2fa"; import { disableUser2fa } from "./commands/disableUser2fa";
import { setServerAdmin } from "./commands/setServerAdmin";
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.scriptName("pangctl") .scriptName("pangctl")
@@ -24,6 +23,5 @@ yargs(hideBin(process.argv))
.command(generateOrgCaKeys) .command(generateOrgCaKeys)
.command(clearCertificates) .command(clearCertificates)
.command(disableUser2fa) .command(disableUser2fa)
.command(setServerAdmin)
.demandCommand() .demandCommand()
.help().argv; .help().argv;
+22 -15
View File
@@ -1,47 +1,54 @@
api: api:
insecure: true insecure: true
dashboard: true dashboard: true
providers: providers:
http: http:
endpoint: http://pangolin:3001/api/v1/traefik-config endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: 5s pollInterval: "5s"
file: file:
filename: /etc/traefik/dynamic_config.yml filename: "/etc/traefik/dynamic_config.yml"
experimental: experimental:
plugins: plugins:
badger: badger:
moduleName: github.com/fosrl/badger moduleName: "github.com/fosrl/badger"
version: v1.4.1 version: "{{.BadgerVersion}}"
log: log:
level: INFO level: "INFO"
format: common format: "common"
maxSize: 100 maxSize: 100
maxBackups: 3 maxBackups: 3
maxAge: 3 maxAge: 3
compress: true compress: true
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:
acme: acme:
httpChallenge: httpChallenge:
entryPoint: web entryPoint: web
email: '{{.LetsEncryptEmail}}' email: "{{.LetsEncryptEmail}}"
storage: /letsencrypt/acme.json storage: "/letsencrypt/acme.json"
caServer: https://acme-v02.api.letsencrypt.org/directory caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints: entryPoints:
web: web:
address: ':80' address: ":80"
websecure: websecure:
address: ':443' address: ":443"
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: 30m readTimeout: "30m"
http: http:
tls: tls:
certResolver: letsencrypt certResolver: "letsencrypt"
encodedCharacters: encodedCharacters:
allowEncodedSlash: true allowEncodedSlash: true
allowEncodedQuestionMark: true allowEncodedQuestionMark: true
serversTransport: serversTransport:
insecureSkipVerify: true insecureSkipVerify: true
ping: ping:
entryPoint: web entryPoint: "web"
+1 -7
View File
@@ -22,8 +22,7 @@ server:
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"] allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false credentials: false
{{if .EnableMaxMind}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}} {{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
{{if .EnableMaxMind}}maxmind_asn_path: "./config/GeoLite2-ASN.mmdb"{{end}}
{{if .EnableEmail}} {{if .EnableEmail}}
email: email:
smtp_host: "{{.EmailSMTPHost}}" smtp_host: "{{.EmailSMTPHost}}"
@@ -37,8 +36,3 @@ flags:
disable_signup_without_invite: true disable_signup_without_invite: true
disable_user_create_org: false disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
{{if .IsPostgreSQL}}
postgres:
connection_string: postgresql://pangolin:{{.IsPostgreSQLPass}}@postgres:5432/pangolin
{{end}}
+2 -64
View File
@@ -1,7 +1,7 @@
name: pangolin name: pangolin
services: services:
pangolin: pangolin:
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{if .IsPostgreSQL}}postgresql-{{end}}{{.PangolinVersion}} image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
container_name: pangolin container_name: pangolin
restart: unless-stopped restart: unless-stopped
deploy: deploy:
@@ -10,20 +10,6 @@ services:
memory: 1g memory: 1g
reservations: reservations:
memory: 256m memory: 256m
{{if or .IsPostgreSQL .IsRedis}}
depends_on:
{{if .IsPostgreSQL}}
postgres:
condition: service_healthy
{{end}}
{{if .IsRedis}}
redis:
condition: service_healthy
{{end}}
networks:
- default
- backend
{{end}}
volumes: volumes:
- ./config:/app/config - ./config:/app/config
healthcheck: healthcheck:
@@ -74,56 +60,8 @@ services:
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates - ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs - ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
{{if .IsPostgreSQL}}
postgres:
image: postgres:18
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: pangolin
POSTGRES_PASSWORD: {{.IsPostgreSQLPass}}
POSTGRES_DB: pangolin
volumes:
- ./postgres18:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pangolin"]
interval: 10s
timeout: 5s
retries: 5
networks:
- backend
{{end}}
{{if .IsRedis}}
redis:
image: redis:8-trixie
container_name: redis
restart: unless-stopped
command: >
redis-server
--save 3600 1000
--appendonly yes
--requirepass {{.IsRedisPass}}
volumes:
- ./redis8:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "{{.IsRedisPass}}", "ping"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
networks:
- backend
{{end}}
networks: networks:
default: default:
driver: bridge driver: bridge
name: pangolin_frontend name: pangolin
{{if .EnableIPv6}} enable_ipv6: true{{end}} {{if .EnableIPv6}} enable_ipv6: true{{end}}
{{if or .IsPostgreSQL .IsRedis}}
backend:
driver: bridge
name: pangolin_backend
internal: true
{{end}}
-6
View File
@@ -1,6 +0,0 @@
{{if .IsRedis}}
redis:
host: "redis"
port: 6379
password: "{{.IsRedisPass}}"
{{end}}
+19 -49
View File
@@ -54,13 +54,9 @@ type Config struct {
InstallGerbil bool InstallGerbil bool
TraefikBouncerKey string TraefikBouncerKey string
DoCrowdsecInstall bool DoCrowdsecInstall bool
EnableMaxMind bool EnableGeoblocking bool
Secret string Secret string
IsEnterprise bool IsEnterprise bool
IsPostgreSQL bool
IsPostgreSQLPass string
IsRedis bool
IsRedisPass string
} }
type SupportedContainer string type SupportedContainer string
@@ -127,11 +123,11 @@ func main() {
fmt.Println("\nConfiguration files created successfully!") fmt.Println("\nConfiguration files created successfully!")
// Download MaxMind Country / ASN database if requested // Download MaxMind database if requested
if config.EnableMaxMind { if config.EnableGeoblocking {
fmt.Println("\n=== Downloading MaxMind Country and ASN Databases ===") fmt.Println("\n=== Downloading MaxMind Database ===")
if err := downloadMaxMindDatabase(); err != nil { if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error downloading MaxMind databases: %v\n", err) fmt.Printf("Error downloading MaxMind database: %v\n", err)
fmt.Println("You can download it manually later if needed.") fmt.Println("You can download it manually later if needed.")
} }
} }
@@ -192,15 +188,15 @@ func main() {
fmt.Println("\n=== MaxMind Database Update ===") fmt.Println("\n=== MaxMind Database Update ===")
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil { if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
fmt.Println("MaxMind GeoLite2 Country database found.") fmt.Println("MaxMind GeoLite2 Country database found.")
if readBool("Would you like to update the MaxMind databases (Country and ASN) to the latest version?", false) { if readBool("Would you like to update the MaxMind database to the latest version?", false) {
if err := downloadMaxMindDatabase(); err != nil { if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error updating MaxMind database: %v\n", err) fmt.Printf("Error updating MaxMind database: %v\n", err)
fmt.Println("You can try updating it manually later if needed.") fmt.Println("You can try updating it manually later if needed.")
} }
} }
} else { } else {
fmt.Println("MaxMind GeoLite2 Country and ASN databases not found.") fmt.Println("MaxMind GeoLite2 Country database not found.")
if readBool("Would you like to download the MaxMind GeoLite2 databases for blocking functionality?", false) { if readBool("Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
if err := downloadMaxMindDatabase(); err != nil { if err := downloadMaxMindDatabase(); err != nil {
fmt.Printf("Error downloading MaxMind database: %v\n", err) fmt.Printf("Error downloading MaxMind database: %v\n", err)
fmt.Println("You can try downloading it manually later if needed.") fmt.Println("You can try downloading it manually later if needed.")
@@ -208,10 +204,8 @@ func main() {
// Now you need to update your config file accordingly to enable geoblocking // Now you need to update your config file accordingly to enable geoblocking
fmt.Print("Please remember to update your config/config.yml file to enable geoblocking! \n\n") fmt.Print("Please remember to update your config/config.yml file to enable geoblocking! \n\n")
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server // add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
// add maxmind_asn_path: "./config/GeoLite2-ASN.mmdb" under server fmt.Println("Add the following line under the 'server' section:")
fmt.Println("Add the following lines under the 'server' section:")
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"") fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
fmt.Println(" maxmind_asn_path: \"./config/GeoLite2-ASN.mmdb\"")
} }
} }
} }
@@ -490,17 +484,6 @@ func collectUserInput() Config {
fmt.Println("\n=== Basic Configuration ===") fmt.Println("\n=== Basic Configuration ===")
config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.") config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
if config.IsEnterprise {
config.IsRedis = readBool("Do you want to run the Redis containers locally? Required for HA.")
if config.IsRedis {
config.IsRedisPass = readPassword("Enter a unique password for the Redis service.")
}
}
config.IsPostgreSQL = readBool("Do you want to run the PostgreSQL containers locally? Otherwise, default to the local SQLite database only.", false)
if config.IsPostgreSQL {
config.IsPostgreSQLPass = readPassword("Enter a unique password for the PostgreSQL pangolin user.")
}
config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "") config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "")
@@ -544,7 +527,7 @@ func collectUserInput() Config {
fmt.Println("\n=== Advanced Configuration ===") fmt.Println("\n=== Advanced Configuration ===")
config.EnableIPv6 = readBool("Is your server IPv6 capable?", true) config.EnableIPv6 = readBool("Is your server IPv6 capable?", true)
config.EnableMaxMind = readBool("Do you want to download the MaxMind GeoLite2 Country and ADN databases for blocking functionality?", true) config.EnableGeoblocking = readBool("Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
if config.DashboardDomain == "" { if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required") fmt.Println("Error: Dashboard Domain name is required")
@@ -797,42 +780,29 @@ func checkPortsAvailable(port int) error {
} }
func downloadMaxMindDatabase() error { func downloadMaxMindDatabase() error {
fmt.Println("Downloading MaxMind GeoLite2 Country and ASN databases...") fmt.Println("Downloading MaxMind GeoLite2 Country database...")
// Download the GeoLite2 Country databases // Download the GeoLite2 Country database
if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz", if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil { "https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
return fmt.Errorf("failed to download GeoLite2 Country database: %v", err) return fmt.Errorf("failed to download GeoLite2 database: %v", err)
}
if err := run("curl", "-L", "-o", "GeoLite2-ASN.tar.gz",
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-ASN.tar.gz"); err != nil {
return fmt.Errorf("failed to download GeoLite2 ASN database: %v", err)
} }
// Extract the Country database // Extract the database
if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil { if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
return fmt.Errorf("failed to extract GeoLite2 Country database: %v", err) return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
}
if err := run("tar", "-xzf", "GeoLite2-ASN.tar.gz"); err != nil {
return fmt.Errorf("failed to extract GeoLite2 ASN database: %v", err)
} }
// Find the .mmdb file and move it to the config directory // Find the .mmdb file and move it to the config directory
if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil { if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil {
return fmt.Errorf("failed to move GeoLite2 Country database to config directory: %v", err) return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err)
}
if err := run("bash", "-c", "mv GeoLite2-ASN_*/GeoLite2-ASN.mmdb config/"); err != nil {
return fmt.Errorf("failed to move GeoLite2 ASN database to config directory: %v", err)
} }
// Clean up the downloaded files // Clean up the downloaded files
if err := run("sh", "-c", "rm -rf GeoLite2-Country.tar.gz GeoLite2-Country_*"); err != nil { if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil {
fmt.Printf("Warning: failed to clean up temporary country files: %v\n", err) fmt.Printf("Warning: failed to clean up temporary files: %v\n", err)
}
if err := run("sh", "-c", "rm -rf GeoLite2-ASN.tar.gz GeoLite2-ASN_*"); err != nil {
fmt.Printf("Warning: failed to clean up temporary ASN files: %v\n", err)
} }
fmt.Println("MaxMind GeoLite2 Country and ASN database downloaded successfully!") fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!")
return nil return nil
} }
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Метод", "editInternalResourceDialogScheme": "Метод",
"editInternalResourceDialogEnableSsl": "Активирайте TLS", "editInternalResourceDialogEnableSsl": "Активирайте TLS",
"editInternalResourceDialogEnableSslDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целта.", "editInternalResourceDialogEnableSslDescription": "Активирайте SSL/TLS криптиране за сигурни HTTPS връзки към целта.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Метод", "scheme": "Метод",
"createInternalResourceDialogScheme": "Метод", "createInternalResourceDialogScheme": "Метод",
"createInternalResourceDialogEnableSsl": "Активирайте TLS", "createInternalResourceDialogEnableSsl": "Активирайте TLS",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Schéma", "editInternalResourceDialogScheme": "Schéma",
"editInternalResourceDialogEnableSsl": "Povolit SSL", "editInternalResourceDialogEnableSsl": "Povolit SSL",
"editInternalResourceDialogEnableSslDescription": "Povolit šifrování SSL/TLS pro zabezpečené HTTPS připojení k cíli.", "editInternalResourceDialogEnableSslDescription": "Povolit šifrování SSL/TLS pro zabezpečené HTTPS připojení k cíli.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Schéma", "scheme": "Schéma",
"createInternalResourceDialogScheme": "Schéma", "createInternalResourceDialogScheme": "Schéma",
"createInternalResourceDialogEnableSsl": "Povolit SSL", "createInternalResourceDialogEnableSsl": "Povolit SSL",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Schema", "editInternalResourceDialogScheme": "Schema",
"editInternalResourceDialogEnableSsl": "TLS aktivieren", "editInternalResourceDialogEnableSsl": "TLS aktivieren",
"editInternalResourceDialogEnableSslDescription": "SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zum Ziel aktivieren.", "editInternalResourceDialogEnableSslDescription": "SSL/TLS-Verschlüsselung für sichere HTTPS-Verbindungen zum Ziel aktivieren.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Schema", "scheme": "Schema",
"createInternalResourceDialogScheme": "Schema", "createInternalResourceDialogScheme": "Schema",
"createInternalResourceDialogEnableSsl": "TLS aktivieren", "createInternalResourceDialogEnableSsl": "TLS aktivieren",
+31 -115
View File
@@ -176,7 +176,6 @@
"shareErrorCreateDescription": "An error occurred while creating the share link", "shareErrorCreateDescription": "An error occurred while creating the share link",
"shareCreateDescription": "Anyone with this link can access the resource", "shareCreateDescription": "Anyone with this link can access the resource",
"shareTitleOptional": "Title (optional)", "shareTitleOptional": "Title (optional)",
"sharePathOptional": "Path (optional)",
"expireIn": "Expire In", "expireIn": "Expire In",
"neverExpire": "Never expire", "neverExpire": "Never expire",
"shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.", "shareExpireDescription": "Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource.",
@@ -209,33 +208,11 @@
"resourcesSearch": "Search resources...", "resourcesSearch": "Search resources...",
"resourceAdd": "Add Resource", "resourceAdd": "Add Resource",
"resourceErrorDelte": "Error deleting resource", "resourceErrorDelte": "Error deleting resource",
"resourcePoliciesTitle": "Manage Resource Policies",
"resourcePoliciesAttachedResourcesColumnTitle": "Attached resources",
"resourcePoliciesAttachedResources": "{count} resource(s)",
"resourcePoliciesAttachedResourcesEmpty": "no resources",
"resourcePoliciesDescription": "Create and manage authentication policies to control access to your resources",
"resourcePoliciesSearch": "Search policies...",
"resourcePoliciesAdd": "Add Policy",
"resourcePoliciesDefaultBadgeText": "Default policy",
"resourcePoliciesCreate": "Create Resource Policy",
"resourcePoliciesCreateDescription": "Follow the steps below to create a new policy",
"resourcePolicyName": "Policy Name",
"resourcePolicyNameDescription": "Give this policy a name to identify it across your resources",
"resourcePolicyNamePlaceholder": "e.g. Internal Access Policy",
"resourcePoliciesSeeAll": "See All Policies",
"resourcePolicyAuthMethodAdd": "Add Authentication Method",
"resourcePolicyOtpEmailAdd": "Add OTP emails",
"resourcePolicyRulesAdd": "Add Rules",
"resourcePolicyAuthMethodsDescription": "Allow access to resources via additional auth methods",
"resourcePolicyUsersRolesDescription": "Configure which users and roles can visit associated resources",
"rulesResourcePolicyDescription": "Configure rules to control access resources associated to this policy",
"authentication": "Authentication", "authentication": "Authentication",
"protected": "Protected", "protected": "Protected",
"notProtected": "Not Protected", "notProtected": "Not Protected",
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.", "resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?", "resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
"resourcePolicyMessageRemove": "Once removed, the resource policy will no longer be accessible. All resources associated with the resource will be unlinked and left without authentication.",
"resourcePolicyQuestionRemove": "Are you sure you want to remove the resource policy from the organization?",
"resourceHTTP": "HTTPS Resource", "resourceHTTP": "HTTPS Resource",
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.", "resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
"resourceRaw": "Raw TCP/UDP Resource", "resourceRaw": "Raw TCP/UDP Resource",
@@ -243,9 +220,8 @@
"resourceRawDescriptionCloud": "Proxy requests over raw TCP/UDP using a port number. Requires sites to connect to a remote node.", "resourceRawDescriptionCloud": "Proxy requests over raw TCP/UDP using a port number. Requires sites to connect to a remote node.",
"resourceCreate": "Create Resource", "resourceCreate": "Create Resource",
"resourceCreateDescription": "Follow the steps below to create a new resource", "resourceCreateDescription": "Follow the steps below to create a new resource",
"resourceCreateGeneralDescription": "Configure the basic resource settings including the name and the type",
"resourceSeeAll": "See All Resources", "resourceSeeAll": "See All Resources",
"resourceCreateGeneral": "General", "resourceInfo": "Resource Information",
"resourceNameDescription": "This is the display name for the resource.", "resourceNameDescription": "This is the display name for the resource.",
"siteSelect": "Select site", "siteSelect": "Select site",
"siteSearch": "Search site", "siteSearch": "Search site",
@@ -255,15 +231,12 @@
"noCountryFound": "No country found.", "noCountryFound": "No country found.",
"siteSelectionDescription": "This site will provide connectivity to the target.", "siteSelectionDescription": "This site will provide connectivity to the target.",
"resourceType": "Resource Type", "resourceType": "Resource Type",
"resourceTypeDescription": "This controls the resource protocol and how it will be rendered in the browser. This cant be changed later.", "resourceTypeDescription": "Determine how to access the resource",
"resourceDomainDescription": "The resource will be served at this fully qualified domain name.",
"resourceHTTPSSettings": "HTTPS Settings", "resourceHTTPSSettings": "HTTPS Settings",
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS", "resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
"resourcePortDescription": "The external port on the Pangolin instance or node where the resource will be accessible.",
"domainType": "Domain Type", "domainType": "Domain Type",
"subdomain": "Subdomain", "subdomain": "Subdomain",
"baseDomain": "Base Domain", "baseDomain": "Base Domain",
"configure": "Configure",
"subdomnainDescription": "The subdomain where the resource will be accessible.", "subdomnainDescription": "The subdomain where the resource will be accessible.",
"resourceRawSettings": "TCP/UDP Settings", "resourceRawSettings": "TCP/UDP Settings",
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP", "resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP",
@@ -280,8 +253,6 @@
"resourceLearnRaw": "Learn how to configure TCP/UDP resources", "resourceLearnRaw": "Learn how to configure TCP/UDP resources",
"resourceBack": "Back to Resources", "resourceBack": "Back to Resources",
"resourceGoTo": "Go to Resource", "resourceGoTo": "Go to Resource",
"resourcePolicyDelete": "Delete Resource Policy",
"resourcePolicyDeleteConfirm": "Confirm Delete Resource Policy",
"resourceDelete": "Delete Resource", "resourceDelete": "Delete Resource",
"resourceDeleteConfirm": "Confirm Delete Resource", "resourceDeleteConfirm": "Confirm Delete Resource",
"labelDelete": "Delete Label", "labelDelete": "Delete Label",
@@ -311,8 +282,6 @@
"rules": "Rules", "rules": "Rules",
"resourceSettingDescription": "Configure the settings on the resource", "resourceSettingDescription": "Configure the settings on the resource",
"resourceSetting": "{resourceName} Settings", "resourceSetting": "{resourceName} Settings",
"resourcePolicySettingDescription": "Configure the settings on the resource policy",
"resourcePolicySetting": "{policyName} Settings",
"alwaysAllow": "Bypass Auth", "alwaysAllow": "Bypass Auth",
"alwaysDeny": "Block Access", "alwaysDeny": "Block Access",
"passToAuth": "Pass to Auth", "passToAuth": "Pass to Auth",
@@ -795,16 +764,6 @@
"rulesNoOne": "No rules. Add a rule using the form.", "rulesNoOne": "No rules. Add a rule using the form.",
"rulesOrder": "Rules are evaluated by priority in ascending order.", "rulesOrder": "Rules are evaluated by priority in ascending order.",
"rulesSubmit": "Save Rules", "rulesSubmit": "Save Rules",
"policyErrorCreate": "Error creating policy",
"policyErrorCreateDescription": "An error occurred when creating the policy",
"policyErrorCreateMessageDescription": "An unexpected error occurred",
"policyErrorUpdate": "Error updating policy",
"policyErrorUpdateDescription": "An error occurred when updating the policy",
"policyErrorUpdateMessageDescription": "An unexpected error occurred",
"policyCreatedSuccess": "Resource policy succesfully created",
"policyUpdatedSuccess": "Resource policy succesfully updated",
"authMethodsSave": "Save auth methods",
"rulesSave": "Save Rules",
"resourceErrorCreate": "Error creating resource", "resourceErrorCreate": "Error creating resource",
"resourceErrorCreateDescription": "An error occurred when creating the resource", "resourceErrorCreateDescription": "An error occurred when creating the resource",
"resourceErrorCreateMessage": "Error creating resource:", "resourceErrorCreateMessage": "Error creating resource:",
@@ -868,17 +827,6 @@
"pincodeAdd": "Add PIN Code", "pincodeAdd": "Add PIN Code",
"pincodeRemove": "Remove PIN Code", "pincodeRemove": "Remove PIN Code",
"resourceAuthMethods": "Authentication Methods", "resourceAuthMethods": "Authentication Methods",
"resourcePolicyAuthMethodsEmpty": "No authentication method",
"resourcePolicyOtpEmpty": "No one time password",
"resourcePolicyReadOnly": "This policy is Read only",
"resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it on this page.",
"editSharedPolicy": "Edit Shared Policy",
"resourcePolicyTypeSave": "Save Resource type",
"resourcePolicySelect": "Select resource policy",
"resourcePolicySelectError": "Select a resource policy",
"resourcePolicyNotFound": "Policy not found",
"resourcePolicySearch": "Search policies",
"resourcePolicyRulesEmpty": "No authentication rules",
"resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods", "resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods",
"resourceAuthSettingsSave": "Saved successfully", "resourceAuthSettingsSave": "Saved successfully",
"resourceAuthSettingsSaveDescription": "Authentication settings have been saved", "resourceAuthSettingsSaveDescription": "Authentication settings have been saved",
@@ -914,12 +862,6 @@
"resourcePincodeSetupTitle": "Set Pincode", "resourcePincodeSetupTitle": "Set Pincode",
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource", "resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
"resourceRoleDescription": "Admins can always access this resource.", "resourceRoleDescription": "Admins can always access this resource.",
"resourcePolicySelectTitle": "Resource Access Policy",
"resourcePolicySelectDescription": "Select the resource policy type for authentication",
"resourcePolicyInline": "Inline Resource Policy",
"resourcePolicyInlineDescription": "Access Policy scoped to only this resource",
"resourcePolicyShared": "Shared Resource Policy",
"resourcePolicySharedDescription": "This resource uses a shared policy. Policy-level settings (auth methods, email whitelist) are locked. You can add resource-specific rules, roles, and users below.",
"resourceUsersRoles": "Access Controls", "resourceUsersRoles": "Access Controls",
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource", "resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
"resourceUsersRolesSubmit": "Save Access Controls", "resourceUsersRolesSubmit": "Save Access Controls",
@@ -1412,6 +1354,29 @@
"otpAuthBack": "Back to Password", "otpAuthBack": "Back to Password",
"navbar": "Navigation Menu", "navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application", "navbarDescription": "Main navigation menu for the application",
"commandPaletteTitle": "Command palette",
"commandPaletteDescription": "Search for pages, organizations, resources, and actions",
"commandPaletteSearchPlaceholder": "Search pages, resources, actions...",
"commandPaletteNoResults": "No results found.",
"commandPaletteSearching": "Searching...",
"commandPaletteNavigation": "Navigation",
"commandPaletteOrganizations": "Organizations",
"commandPaletteSites": "Sites",
"commandPaletteResources": "Resources",
"commandPaletteUsers": "Users",
"commandPaletteClients": "Machine clients",
"commandPaletteActions": "Actions",
"commandPaletteCreateSite": "Create site",
"commandPaletteCreateProxyResource": "Create public resource",
"commandPaletteCreateUser": "Create user",
"commandPaletteCreateApiKey": "Create API key",
"commandPaletteCreateMachineClient": "Create machine client",
"commandPaletteCreateAlertRule": "Create alert rule",
"commandPaletteCreateIdentityProvider": "Create identity provider",
"commandPaletteToggleTheme": "Toggle theme",
"commandPaletteChooseOrganization": "Choose organization",
"commandPaletteShortcutMac": "⌘K",
"commandPaletteShortcutWindows": "Ctrl K",
"navbarDocsLink": "Documentation", "navbarDocsLink": "Documentation",
"otpErrorEnable": "Unable to enable 2FA", "otpErrorEnable": "Unable to enable 2FA",
"otpErrorEnableDescription": "An error occurred while enabling 2FA", "otpErrorEnableDescription": "An error occurred while enabling 2FA",
@@ -1461,8 +1426,6 @@
"sidebarResources": "Resources", "sidebarResources": "Resources",
"sidebarProxyResources": "Public", "sidebarProxyResources": "Public",
"sidebarClientResources": "Private", "sidebarClientResources": "Private",
"sidebarPolicies": "Policies",
"sidebarResourcePolicies": "Resources",
"sidebarAccessControl": "Access Control", "sidebarAccessControl": "Access Control",
"sidebarLogsAndAnalytics": "Logs & Analytics", "sidebarLogsAndAnalytics": "Logs & Analytics",
"sidebarTeam": "Team", "sidebarTeam": "Team",
@@ -1646,8 +1609,7 @@
"standaloneHcFilterSiteIdFallback": "Site {id}", "standaloneHcFilterSiteIdFallback": "Site {id}",
"standaloneHcFilterResourceIdFallback": "Resource {id}", "standaloneHcFilterResourceIdFallback": "Resource {id}",
"blueprints": "Blueprints", "blueprints": "Blueprints",
"blueprintsLog": "Blueprints Log", "blueprintsDescription": "Apply declarative configurations and view previous runs",
"blueprintsDescription": "View past blueprint applications and their results",
"blueprintAdd": "Add Blueprint", "blueprintAdd": "Add Blueprint",
"blueprintGoBack": "See all Blueprints", "blueprintGoBack": "See all Blueprints",
"blueprintCreate": "Create Blueprint", "blueprintCreate": "Create Blueprint",
@@ -1665,17 +1627,7 @@
"contents": "Contents", "contents": "Contents",
"parsedContents": "Parsed Contents (Read Only)", "parsedContents": "Parsed Contents (Read Only)",
"enableDockerSocket": "Enable Docker Blueprint", "enableDockerSocket": "Enable Docker Blueprint",
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to the site connector. Read about how this works in <docsLink>the documentation</docsLink>.", "enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in <docsLink>the documentation</docsLink>.",
"newtAutoUpdate": "Enable Site Auto-Update",
"newtAutoUpdateDescription": "When enabled, site connectors will automatically update to the latest version when a new release is available.",
"siteAutoUpdate": "Site Auto-Update",
"siteAutoUpdateLabel": "Enable Auto-Update",
"siteAutoUpdateDescription": "Control whether this site's connector automatically downloads the latest version.",
"siteAutoUpdateOrgDefault": "Organization default: {state}",
"siteAutoUpdateOverriding": "Overriding organization setting",
"siteAutoUpdateResetToOrg": "Reset to Organization Default",
"siteAutoUpdateEnabled": "enabled",
"siteAutoUpdateDisabled": "disabled",
"viewDockerContainers": "View Docker Containers", "viewDockerContainers": "View Docker Containers",
"containersIn": "Containers in {siteName}", "containersIn": "Containers in {siteName}",
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.", "selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
@@ -1947,7 +1899,6 @@
"billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys", "billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys",
"billingCurrentKeys": "Current Keys", "billingCurrentKeys": "Current Keys",
"billingModifyCurrentPlan": "Modify Current Plan", "billingModifyCurrentPlan": "Modify Current Plan",
"billingManageLicenseSubscriptionDescription": "Manage your subscription for paid self-hosted license keys and download invoices.",
"billingConfirmUpgrade": "Confirm Upgrade", "billingConfirmUpgrade": "Confirm Upgrade",
"billingConfirmDowngrade": "Confirm Downgrade", "billingConfirmDowngrade": "Confirm Downgrade",
"billingConfirmUpgradeDescription": "You are about to upgrade your plan. Review the new limits and pricing below.", "billingConfirmUpgradeDescription": "You are about to upgrade your plan. Review the new limits and pricing below.",
@@ -2045,36 +1996,7 @@
"timeIsInSeconds": "Time is in seconds", "timeIsInSeconds": "Time is in seconds",
"requireDeviceApproval": "Require Device Approvals", "requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.", "requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
"sshSettings": "SSH Settings", "sshAccess": "SSH Access",
"rdpSettings": "RDP Settings",
"vncSettings": "VNC Settings",
"sshServer": "SSH Server",
"rdpServer": "RDP Server",
"vncServer": "VNC Server",
"sshServerDescription": "Set up the authentication method, daemon location, and server destination",
"rdpServerDescription": "Configure the destination and port of the RDP server",
"vncServerDescription": "Configure the destination and port of the VNC server",
"sshServerMode": "Mode",
"sshServerModeStandard": "Standard SSH Server",
"sshServerModePangolin": "Pangolin SSH",
"sshServerModeStandardDescription": "Routes commands over network to an SSH server such as OpenSSH.",
"sshServerModeNative": "Native SSH Server",
"sshServerModeNativeDescription": "Executes commands directly on the host via the Site Connector. No network config required.",
"sshAuthenticationMethod": "Authentication Method",
"sshAuthMethodManual": "Manual Authentication",
"sshAuthMethodManualDescription": "Requires existing host credentials. Bypasses automatic provisioning.",
"sshAuthMethodAutomated": "Automated Provisioning",
"sshAuthMethodAutomatedDescription": "Automatically creates users, groups, and sudo permissions on host.",
"sshAuthDaemonLocation": "Auth Daemon Location",
"sshDaemonLocationSiteDescription": "Executes locally on the machine hosting the site connector.",
"sshDaemonLocationRemote": "On Remote Host",
"sshDaemonLocationRemoteDescription": "Executes on a separate target machine on the same network.",
"sshDaemonDisclaimer": "Ensure your target host is properly configured to run the auth daemon before completing this setup, or provisioning will fail.",
"sshDaemonPort": "Daemon Port",
"sshServerDestination": "Server Destination",
"sshServerDestinationDescription": "Configure the destination and port of the SSH server",
"destination": "Destination",
"bgTargetMultiSiteDisclaimer": "Selecting multiple sites enables resilient routing and failover for high availability.",
"roleAllowSsh": "Allow SSH", "roleAllowSsh": "Allow SSH",
"roleAllowSshAllow": "Allow", "roleAllowSshAllow": "Allow",
"roleAllowSshDisallow": "Disallow", "roleAllowSshDisallow": "Disallow",
@@ -2088,7 +2010,7 @@
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.", "sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
"sshSudo": "Allow sudo", "sshSudo": "Allow sudo",
"sshSudoCommands": "Sudo Commands", "sshSudoCommands": "Sudo Commands",
"sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo. Absolute paths must be used.", "sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo.",
"sshCreateHomeDir": "Create Home Directory", "sshCreateHomeDir": "Create Home Directory",
"sshUnixGroups": "Unix Groups", "sshUnixGroups": "Unix Groups",
"sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.", "sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.",
@@ -2180,7 +2102,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Scheme", "editInternalResourceDialogScheme": "Scheme",
"editInternalResourceDialogEnableSsl": "Enable TLS", "editInternalResourceDialogEnableSsl": "Enable TLS",
"editInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.", "editInternalResourceDialogEnableSslDescription": "Enable SSL/TLS encryption for secure HTTPS connections to the destination.",
@@ -2230,7 +2151,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Scheme", "scheme": "Scheme",
"createInternalResourceDialogScheme": "Scheme", "createInternalResourceDialogScheme": "Scheme",
"createInternalResourceDialogEnableSsl": "Enable TLS", "createInternalResourceDialogEnableSsl": "Enable TLS",
@@ -3070,7 +2990,7 @@
"learnMore": "Learn more", "learnMore": "Learn more",
"backToHome": "Go back to home", "backToHome": "Go back to home",
"needToSignInToOrg": "Need to use your organization's identity provider?", "needToSignInToOrg": "Need to use your organization's identity provider?",
"maintenanceMode": "Maintenance Page", "maintenanceMode": "Maintenance Mode",
"maintenanceModeDescription": "Display a maintenance page to visitors", "maintenanceModeDescription": "Display a maintenance page to visitors",
"maintenanceModeType": "Maintenance Mode Type", "maintenanceModeType": "Maintenance Mode Type",
"showMaintenancePage": "Show a maintenance page to visitors", "showMaintenancePage": "Show a maintenance page to visitors",
@@ -3100,7 +3020,6 @@
"maintenanceScreenEstimatedCompletion": "Estimated Completion:", "maintenanceScreenEstimatedCompletion": "Estimated Completion:",
"createInternalResourceDialogDestinationRequired": "Destination is required", "createInternalResourceDialogDestinationRequired": "Destination is required",
"available": "Available", "available": "Available",
"disabledResourceDescription": "When disabled, the resource will be inaccessible by everyone.",
"archived": "Archived", "archived": "Archived",
"noArchivedDevices": "No archived devices found", "noArchivedDevices": "No archived devices found",
"deviceArchived": "Device archived", "deviceArchived": "Device archived",
@@ -3430,8 +3349,5 @@
"memberPortalResourceDisabled": "Resource Disabled", "memberPortalResourceDisabled": "Resource Disabled",
"memberPortalShowingResources": "Showing {start}-{end} of {total} resources", "memberPortalShowingResources": "Showing {start}-{end} of {total} resources",
"memberPortalPrevious": "Previous", "memberPortalPrevious": "Previous",
"memberPortalNext": "Next", "memberPortalNext": "Next"
"httpSettings": "HTTP Settings",
"tcpSettings": "TCP Settings",
"udpSettings": "UDP Settings"
} }
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Esquema", "editInternalResourceDialogScheme": "Esquema",
"editInternalResourceDialogEnableSsl": "Activar TLS", "editInternalResourceDialogEnableSsl": "Activar TLS",
"editInternalResourceDialogEnableSslDescription": "Habilitar cifrado SSL/TLS para conexiones HTTPS seguras al destino.", "editInternalResourceDialogEnableSslDescription": "Habilitar cifrado SSL/TLS para conexiones HTTPS seguras al destino.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Esquema", "scheme": "Esquema",
"createInternalResourceDialogScheme": "Esquema", "createInternalResourceDialogScheme": "Esquema",
"createInternalResourceDialogEnableSsl": "Activar TLS", "createInternalResourceDialogEnableSsl": "Activar TLS",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Méthode HTTP", "editInternalResourceDialogScheme": "Méthode HTTP",
"editInternalResourceDialogEnableSsl": "Activer TLS", "editInternalResourceDialogEnableSsl": "Activer TLS",
"editInternalResourceDialogEnableSslDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers la destination.", "editInternalResourceDialogEnableSslDescription": "Activer le cryptage SSL/TLS pour des connexions HTTPS sécurisées vers la destination.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Méthode HTTP", "scheme": "Méthode HTTP",
"createInternalResourceDialogScheme": "Méthode HTTP", "createInternalResourceDialogScheme": "Méthode HTTP",
"createInternalResourceDialogEnableSsl": "Activer TLS", "createInternalResourceDialogEnableSsl": "Activer TLS",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Metodo HTTP", "editInternalResourceDialogScheme": "Metodo HTTP",
"editInternalResourceDialogEnableSsl": "Abilitare TLS", "editInternalResourceDialogEnableSsl": "Abilitare TLS",
"editInternalResourceDialogEnableSslDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alla destinazione.", "editInternalResourceDialogEnableSslDescription": "Abilita la crittografia SSL/TLS per connessioni HTTPS sicure alla destinazione.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Metodo HTTP", "scheme": "Metodo HTTP",
"createInternalResourceDialogScheme": "Metodo HTTP", "createInternalResourceDialogScheme": "Metodo HTTP",
"createInternalResourceDialogEnableSsl": "Abilitare TLS", "createInternalResourceDialogEnableSsl": "Abilitare TLS",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "스킴", "editInternalResourceDialogScheme": "스킴",
"editInternalResourceDialogEnableSsl": "TLS 활성화", "editInternalResourceDialogEnableSsl": "TLS 활성화",
"editInternalResourceDialogEnableSslDescription": "목적지로의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화 활성화.", "editInternalResourceDialogEnableSslDescription": "목적지로의 안전한 HTTPS 연결을 위한 SSL/TLS 암호화 활성화.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "스킴", "scheme": "스킴",
"createInternalResourceDialogScheme": "스킴", "createInternalResourceDialogScheme": "스킴",
"createInternalResourceDialogEnableSsl": "TLS 활성화", "createInternalResourceDialogEnableSsl": "TLS 활성화",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Skjema", "editInternalResourceDialogScheme": "Skjema",
"editInternalResourceDialogEnableSsl": "Aktiver TLS", "editInternalResourceDialogEnableSsl": "Aktiver TLS",
"editInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.", "editInternalResourceDialogEnableSslDescription": "Aktiver SSL/TLS-kryptering for sikre HTTPS-tilkoblinger til destinasjonen.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Skjema", "scheme": "Skjema",
"createInternalResourceDialogScheme": "Skjema", "createInternalResourceDialogScheme": "Skjema",
"createInternalResourceDialogEnableSsl": "Aktiver TLS", "createInternalResourceDialogEnableSsl": "Aktiver TLS",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Schema", "editInternalResourceDialogScheme": "Schema",
"editInternalResourceDialogEnableSsl": "TLS inschakelen", "editInternalResourceDialogEnableSsl": "TLS inschakelen",
"editInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.", "editInternalResourceDialogEnableSslDescription": "Schakel SSL/TLS-encryptie in voor beveiligde HTTPS-verbindingen met de bestemming.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Schema", "scheme": "Schema",
"createInternalResourceDialogScheme": "Schema", "createInternalResourceDialogScheme": "Schema",
"createInternalResourceDialogEnableSsl": "TLS inschakelen", "createInternalResourceDialogEnableSsl": "TLS inschakelen",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Schemat", "editInternalResourceDialogScheme": "Schemat",
"editInternalResourceDialogEnableSsl": "Włącz TLS", "editInternalResourceDialogEnableSsl": "Włącz TLS",
"editInternalResourceDialogEnableSslDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z miejscem docelowym.", "editInternalResourceDialogEnableSslDescription": "Włącz szyfrowanie SSL/TLS dla bezpiecznych połączeń HTTPS z miejscem docelowym.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Schemat", "scheme": "Schemat",
"createInternalResourceDialogScheme": "Schemat", "createInternalResourceDialogScheme": "Schemat",
"createInternalResourceDialogEnableSsl": "Włącz TLS", "createInternalResourceDialogEnableSsl": "Włącz TLS",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Esquema", "editInternalResourceDialogScheme": "Esquema",
"editInternalResourceDialogEnableSsl": "Ativar TLS", "editInternalResourceDialogEnableSsl": "Ativar TLS",
"editInternalResourceDialogEnableSslDescription": "Ativar criptografia SSL/TLS para conexões HTTPS seguras com o destino.", "editInternalResourceDialogEnableSslDescription": "Ativar criptografia SSL/TLS para conexões HTTPS seguras com o destino.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Esquema", "scheme": "Esquema",
"createInternalResourceDialogScheme": "Esquema", "createInternalResourceDialogScheme": "Esquema",
"createInternalResourceDialogEnableSsl": "Ativar TLS", "createInternalResourceDialogEnableSsl": "Ativar TLS",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "СИДР", "editInternalResourceDialogModeCidr": "СИДР",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Схема", "editInternalResourceDialogScheme": "Схема",
"editInternalResourceDialogEnableSsl": "Включить TLS", "editInternalResourceDialogEnableSsl": "Включить TLS",
"editInternalResourceDialogEnableSslDescription": "Включите шифрование SSL/TLS для защищенных HTTPS соединений с конечной точкой.", "editInternalResourceDialogEnableSslDescription": "Включите шифрование SSL/TLS для защищенных HTTPS соединений с конечной точкой.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "СИДР", "createInternalResourceDialogModeCidr": "СИДР",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Схема", "scheme": "Схема",
"createInternalResourceDialogScheme": "Схема", "createInternalResourceDialogScheme": "Схема",
"createInternalResourceDialogEnableSsl": "Включить TLS", "createInternalResourceDialogEnableSsl": "Включить TLS",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Şema", "editInternalResourceDialogScheme": "Şema",
"editInternalResourceDialogEnableSsl": "TLS Etkinleştir", "editInternalResourceDialogEnableSsl": "TLS Etkinleştir",
"editInternalResourceDialogEnableSslDescription": "Hedefe güvenli HTTPS bağlantıları için SSL/TLS şifrelemeyi etkinleştirin.", "editInternalResourceDialogEnableSslDescription": "Hedefe güvenli HTTPS bağlantıları için SSL/TLS şifrelemeyi etkinleştirin.",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "Şema", "scheme": "Şema",
"createInternalResourceDialogScheme": "Şema", "createInternalResourceDialogScheme": "Şema",
"createInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir", "createInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir",
-2
View File
@@ -2049,7 +2049,6 @@
"editInternalResourceDialogModeCidr": "CIDR", "editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP", "editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS", "editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "方案", "editInternalResourceDialogScheme": "方案",
"editInternalResourceDialogEnableSsl": "启用 TLS", "editInternalResourceDialogEnableSsl": "启用 TLS",
"editInternalResourceDialogEnableSslDescription": "为目标的安全 HTTPS 连接启用 SSL/TLS 加密。", "editInternalResourceDialogEnableSslDescription": "为目标的安全 HTTPS 连接启用 SSL/TLS 加密。",
@@ -2099,7 +2098,6 @@
"createInternalResourceDialogModeCidr": "CIDR", "createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogModeHttp": "HTTP", "createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS", "createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"scheme": "方案", "scheme": "方案",
"createInternalResourceDialogScheme": "方案", "createInternalResourceDialogScheme": "方案",
"createInternalResourceDialogEnableSsl": "启用 TLS", "createInternalResourceDialogEnableSsl": "启用 TLS",
+1 -31
View File
@@ -1,42 +1,12 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin"; import createNextIntlPlugin from "next-intl/plugin";
import fs from "fs";
import path from "path";
const withNextIntl = createNextIntlPlugin(); const withNextIntl = createNextIntlPlugin();
// read allowedDevOrigins.json if it exists
let allowedDevOrigins: string[] = [];
const allowedDevOriginsPath = path.join(
process.cwd(),
"allowedDevOrigins.json"
);
if (fs.existsSync(allowedDevOriginsPath)) {
try {
const data = fs.readFileSync(allowedDevOriginsPath, "utf-8");
allowedDevOrigins = JSON.parse(data);
} catch {}
}
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
reactStrictMode: false, reactStrictMode: false,
reactCompiler: true, reactCompiler: true,
transpilePackages: ["@novnc/novnc"], output: "standalone"
output: "standalone",
allowedDevOrigins,
async redirects() {
return [
{
source: "/:orgId/settings/resources/proxy/:path*",
destination: "/:orgId/settings/resources/public/:path*",
permanent: true
},
{
source: "/:orgId/settings/resources/client/:path*",
destination: "/:orgId/settings/resources/private/:path*",
permanent: true
}
];
}
}; };
export default withNextIntl(nextConfig); export default withNextIntl(nextConfig);
+733 -643
View File
File diff suppressed because it is too large Load Diff
+28 -32
View File
@@ -33,14 +33,12 @@
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "8.5.0", "@asteasolutions/zod-to-openapi": "8.5.0",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", "@aws-sdk/client-s3": "3.1047.0",
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz", "@faker-js/faker": "10.4.0",
"@aws-sdk/client-s3": "3.1056.0",
"@headlessui/react": "2.2.10", "@headlessui/react": "2.2.10",
"@hookform/resolvers": "5.4.0", "@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
"@node-rs/argon2": "2.0.2", "@node-rs/argon2": "2.0.2",
"@novnc/novnc": "^1.7.0",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.11", "@radix-ui/react-avatar": "1.1.11",
@@ -66,13 +64,10 @@
"@react-email/render": "2.0.8", "@react-email/render": "2.0.8",
"@react-email/tailwind": "2.0.7", "@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1", "@simplewebauthn/server": "13.3.0",
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.100.14", "@tanstack/react-query": "5.100.10",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "8.21.3",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"arctic": "3.7.0", "arctic": "3.7.0",
"axios": "1.16.1", "axios": "1.16.1",
"better-sqlite3": "11.9.1", "better-sqlite3": "11.9.1",
@@ -88,37 +83,38 @@
"express": "5.2.1", "express": "5.2.1",
"express-rate-limit": "8.5.2", "express-rate-limit": "8.5.2",
"glob": "13.0.6", "glob": "13.0.6",
"helmet": "8.2.0", "helmet": "8.1.0",
"http-errors": "2.0.1", "http-errors": "2.0.1",
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "5.11.0", "ioredis": "5.10.1",
"jmespath": "0.16.0", "jmespath": "0.16.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3", "jsonwebtoken": "9.0.3",
"lucide-react": "1.17.0", "lucide-react": "0.577.0",
"maxmind": "5.0.6", "maxmind": "5.0.6",
"moment": "2.30.1", "moment": "2.30.1",
"next": "16.2.6", "next": "16.2.6",
"next-intl": "4.13.0", "next-intl": "4.12.0",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"nextjs-toploader": "3.9.17", "nextjs-toploader": "3.9.17",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"nodemailer": "8.0.9", "nodemailer": "8.0.7",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "8.21.0", "pg": "8.20.0",
"posthog-node": "5.35.6", "posthog-node": "5.34.1",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.6", "react": "19.2.6",
"react-day-picker": "9.14.0", "react-day-picker": "9.14.0",
"react-dom": "19.2.6", "react-dom": "19.2.6",
"react-easy-sort": "1.8.0", "react-easy-sort": "1.8.0",
"react-hook-form": "7.76.1", "react-hook-form": "7.75.0",
"react-icons": "5.6.0", "react-icons": "5.6.0",
"recharts": "3.8.1", "recharts": "3.8.1",
"reodotdev": "1.1.0", "reodotdev": "1.1.0",
"semver": "7.8.1", "resend": "6.12.3",
"semver": "7.8.0",
"sshpk": "1.18.0", "sshpk": "1.18.0",
"stripe": "22.2.0", "stripe": "20.4.1",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tailwind-merge": "3.6.0", "tailwind-merge": "3.6.0",
"topojson-client": "3.1.0", "topojson-client": "3.1.0",
@@ -129,18 +125,18 @@
"visionscarto-world-atlas": "1.0.0", "visionscarto-world-atlas": "1.0.0",
"winston": "3.19.0", "winston": "3.19.0",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.21.0", "ws": "8.20.1",
"yaml": "2.9.0", "yaml": "2.9.0",
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "4.4.3", "zod": "4.4.3",
"zod-validation-error": "5.0.0" "zod-validation-error": "5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.69.1", "@dotenvx/dotenvx": "1.66.0",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/ui": "^6.5.0", "@react-email/ui": "^6.1.4",
"@tailwindcss/postcss": "4.3.0", "@tailwindcss/postcss": "4.3.0",
"@tanstack/react-query-devtools": "5.100.14", "@tanstack/react-query-devtools": "5.100.10",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10", "@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",
@@ -151,11 +147,11 @@
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "25.9.1", "@types/node": "25.8.0",
"@types/nodemailer": "8.0.0", "@types/nodemailer": "8.0.0",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/pg": "8.20.0", "@types/pg": "8.20.0",
"@types/react": "19.2.15", "@types/react": "19.2.14",
"@types/react-dom": "19.2.3", "@types/react-dom": "19.2.3",
"@types/semver": "7.7.1", "@types/semver": "7.7.1",
"@types/sshpk": "1.17.4", "@types/sshpk": "1.17.4",
@@ -167,20 +163,20 @@
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"esbuild": "0.28.0", "esbuild": "0.28.0",
"esbuild-node-externals": "1.22.0", "esbuild-node-externals": "1.22.0",
"eslint": "10.4.0", "eslint": "10.3.0",
"eslint-config-next": "16.2.6", "eslint-config-next": "16.2.6",
"postcss": "8.5.15", "postcss": "8.5.14",
"prettier": "3.8.3", "prettier": "3.8.3",
"react-email": "6.5.0", "react-email": "6.1.4",
"tailwindcss": "4.3.0", "tailwindcss": "4.3.0",
"tsc-alias": "1.8.17", "tsc-alias": "1.8.17",
"tsx": "4.22.3", "tsx": "4.22.0",
"typescript": "6.0.3", "typescript": "6.0.3",
"typescript-eslint": "8.60.0" "typescript-eslint": "8.59.3"
}, },
"overrides": { "overrides": {
"esbuild": "0.28.0", "esbuild": "0.28.0",
"dompurify": "3.4.0", "dompurify": "3.4.0",
"postcss": "8.5.15" "postcss": "8.5.14"
} }
} }
+15 -39
View File
@@ -5,7 +5,6 @@ import { and, eq, inArray } from "drizzle-orm";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import logger from "@server/logger";
export enum ActionsEnum { export enum ActionsEnum {
createOrgUser = "createOrgUser", createOrgUser = "createOrgUser",
@@ -159,26 +158,7 @@ export enum ActionsEnum {
createHealthCheck = "createHealthCheck", createHealthCheck = "createHealthCheck",
updateHealthCheck = "updateHealthCheck", updateHealthCheck = "updateHealthCheck",
deleteHealthCheck = "deleteHealthCheck", deleteHealthCheck = "deleteHealthCheck",
listHealthChecks = "listHealthChecks", listHealthChecks = "listHealthChecks"
createBrowserGatewayTarget = "createBrowserGatewayTarget",
updateBrowserGatewayTarget = "updateBrowserGatewayTarget",
deleteBrowserGatewayTarget = "deleteBrowserGatewayTarget",
getBrowserGatewayTarget = "getBrowserGatewayTarget",
listBrowserGatewayTargets = "listBrowserGatewayTargets",
listResourcePolicies = "listResourcePolicies",
getResourcePolicy = "getResourcePolicy",
createResourcePolicy = "createResourcePolicy",
updateResourcePolicy = "updateResourcePolicy",
deleteResourcePolicy = "deleteResourcePolicy",
listResourcePolicyRoles = "listResourcePolicyRoles",
setResourcePolicyRoles = "setResourcePolicyRoles",
listResourcePolicyUsers = "listResourcePolicyUsers",
setResourcePolicyUsers = "setResourcePolicyUsers",
setResourcePolicyPassword = "setResourcePolicyPassword",
setResourcePolicyPincode = "setResourcePolicyPincode",
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
setResourcePolicyRules = "setResourcePolicyRules"
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(
@@ -211,23 +191,6 @@ export async function checkUserActionPermission(
} }
} }
// If no direct permission, check role-based permission (any of user's roles)
const roleActionPermission = await db
.select()
.from(roleActions)
.where(
and(
eq(roleActions.actionId, actionId),
inArray(roleActions.roleId, userOrgRoleIds),
eq(roleActions.orgId, req.userOrgId!)
)
)
.limit(1);
if (roleActionPermission.length > 0) {
return true;
}
// Check if the user has direct permission for the action in the current org // Check if the user has direct permission for the action in the current org
const userActionPermission = await db const userActionPermission = await db
.select() .select()
@@ -245,7 +208,20 @@ export async function checkUserActionPermission(
return true; return true;
} }
return false; // If no direct permission, check role-based permission (any of user's roles)
const roleActionPermission = await db
.select()
.from(roleActions)
.where(
and(
eq(roleActions.actionId, actionId),
inArray(roleActions.roleId, userOrgRoleIds),
eq(roleActions.orgId, req.userOrgId!)
)
)
.limit(1);
return roleActionPermission.length > 0;
} catch (error) { } catch (error) {
console.error("Error checking user action permission:", error); console.error("Error checking user action permission:", error);
throw createHttpError( throw createHttpError(
+1 -10
View File
@@ -19,9 +19,6 @@ export async function createResourceSession(opts: {
userSessionId?: string | null; userSessionId?: string | null;
whitelistId?: number | null; whitelistId?: number | null;
accessTokenId?: string | null; accessTokenId?: string | null;
policyPasswordId?: number | null;
policyPincodeId?: number | null;
policyWhitelistId?: number | null;
doNotExtend?: boolean; doNotExtend?: boolean;
expiresAt?: number | null; expiresAt?: number | null;
sessionLength?: number | null; sessionLength?: number | null;
@@ -31,10 +28,7 @@ export async function createResourceSession(opts: {
!opts.pincodeId && !opts.pincodeId &&
!opts.whitelistId && !opts.whitelistId &&
!opts.accessTokenId && !opts.accessTokenId &&
!opts.userSessionId && !opts.userSessionId
!opts.policyPasswordId &&
!opts.policyPincodeId &&
!opts.policyWhitelistId
) { ) {
throw new Error("Auth method must be provided"); throw new Error("Auth method must be provided");
} }
@@ -55,9 +49,6 @@ export async function createResourceSession(opts: {
whitelistId: opts.whitelistId || null, whitelistId: opts.whitelistId || null,
doNotExtend: opts.doNotExtend || false, doNotExtend: opts.doNotExtend || false,
accessTokenId: opts.accessTokenId || null, accessTokenId: opts.accessTokenId || null,
policyPasswordId: opts.policyPasswordId || null,
policyPincodeId: opts.policyPincodeId || null,
policyWhitelistId: opts.policyWhitelistId || null,
isRequestToken: opts.isRequestToken || false, isRequestToken: opts.isRequestToken || false,
userSessionId: opts.userSessionId || null, userSessionId: opts.userSessionId || null,
issuedAt: new Date().getTime() issuedAt: new Date().getTime()
+1 -36
View File
@@ -1,12 +1,6 @@
import { join } from "path"; import { join } from "path";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { import { clients, db, resources, siteResources } from "@server/db";
clients,
db,
resourcePolicies,
resources,
siteResources
} from "@server/db";
import { randomInt } from "crypto"; import { randomInt } from "crypto";
import { exitNodes, sites } from "@server/db"; import { exitNodes, sites } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
@@ -113,35 +107,6 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
} }
} }
export async function getUniqueResourcePolicyName(
orgId: string
): Promise<string> {
let loops = 0;
while (true) {
if (loops > 100) {
throw new Error("Could not generate a unique name");
}
const name = generateName();
const policyCount = await db
.select({
niceId: resourcePolicies.niceId,
orgId: resourcePolicies.orgId
})
.from(resourcePolicies)
.where(
and(
eq(resourcePolicies.niceId, name),
eq(resourcePolicies.orgId, orgId)
)
);
if (policyCount.length === 0) {
return name;
}
loops++;
}
}
export async function getUniqueSiteResourceName( export async function getUniqueSiteResourceName(
orgId: string orgId: string
): Promise<string> { ): Promise<string> {
-21
View File
@@ -580,24 +580,6 @@ export const trialNotifications = pgTable("trialNotifications", {
sentAt: bigint("sentAt", { mode: "number" }).notNull() sentAt: bigint("sentAt", { mode: "number" }).notNull()
}); });
export const browserGatewayTarget = pgTable("browserGatewayTarget", {
browserGatewayTargetId: serial("browserGatewayTargetId").primaryKey(),
resourceId: integer("resourceId")
.references(() => resources.resourceId, {
onDelete: "cascade"
})
.notNull(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
authToken: varchar("authToken").notNull(),
type: varchar("type").notNull(), // "ssh", "rdp", "vnc"
destination: varchar("destination").notNull(),
destinationPort: integer("destinationPort").notNull()
});
export type Approval = InferSelectModel<typeof approvals>; export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>; export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>; export type Account = InferSelectModel<typeof account>;
@@ -645,6 +627,3 @@ export type AlertEmailRecipients = InferSelectModel<
>; >;
export type AlertWebhookActions = InferSelectModel<typeof alertWebhookActions>; export type AlertWebhookActions = InferSelectModel<typeof alertWebhookActions>;
export type TrialNotification = InferSelectModel<typeof trialNotifications>; export type TrialNotification = InferSelectModel<typeof trialNotifications>;
export type BrowserGatewayTarget = InferSelectModel<
typeof browserGatewayTarget
>;
+8 -160
View File
@@ -65,12 +65,7 @@ export const orgs = pgTable("orgs", {
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format) sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format) sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: boolean("isBillingOrg"), isBillingOrg: boolean("isBillingOrg"),
billingOrgId: varchar("billingOrgId"), billingOrgId: varchar("billingOrgId")
settingsEnableGlobalNewtAutoUpdate: boolean(
"settingsEnableGlobalNewtAutoUpdate"
)
.notNull()
.default(false)
}); });
export const orgDomains = pgTable("orgDomains", { export const orgDomains = pgTable("orgDomains", {
@@ -108,10 +103,6 @@ export const sites = pgTable("sites", {
lastHolePunch: bigint("lastHolePunch", { mode: "number" }), lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
listenPort: integer("listenPort"), listenPort: integer("listenPort"),
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true), dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
autoUpdateEnabled: boolean("autoUpdateEnabled").notNull().default(false),
autoUpdateOverrideOrg: boolean("autoUpdateOverrideOrg")
.notNull()
.default(false),
status: varchar("status") status: varchar("status")
.$type<"pending" | "approved">() .$type<"pending" | "approved">()
.default("approved") .default("approved")
@@ -119,16 +110,6 @@ export const sites = pgTable("sites", {
export const resources = pgTable("resources", { export const resources = pgTable("resources", {
resourceId: serial("resourceId").primaryKey(), resourceId: serial("resourceId").primaryKey(),
resourcePolicyId: integer("resourcePolicyId").references(
() => resourcePolicies.resourcePolicyId,
{ onDelete: "set null" }
),
defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
() => resourcePolicies.resourcePolicyId,
{
onDelete: "restrict"
}
),
resourceGuid: varchar("resourceGuid", { length: 36 }) resourceGuid: varchar("resourceGuid", { length: 36 })
.unique() .unique()
.notNull() .notNull()
@@ -148,6 +129,8 @@ export const resources = pgTable("resources", {
ssl: boolean("ssl").notNull().default(false), ssl: boolean("ssl").notNull().default(false),
blockAccess: boolean("blockAccess").notNull().default(false), blockAccess: boolean("blockAccess").notNull().default(false),
sso: boolean("sso").notNull().default(true), sso: boolean("sso").notNull().default(true),
http: boolean("http").notNull().default(true),
protocol: varchar("protocol").notNull(),
proxyPort: integer("proxyPort"), proxyPort: integer("proxyPort"),
emailWhitelistEnabled: boolean("emailWhitelistEnabled") emailWhitelistEnabled: boolean("emailWhitelistEnabled")
.notNull() .notNull()
@@ -164,6 +147,7 @@ export const resources = pgTable("resources", {
headers: text("headers"), // comma-separated list of headers to add to the request headers: text("headers"), // comma-separated list of headers to add to the request
proxyProtocol: boolean("proxyProtocol").notNull().default(false), proxyProtocol: boolean("proxyProtocol").notNull().default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1), proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
maintenanceModeEnabled: boolean("maintenanceModeEnabled") maintenanceModeEnabled: boolean("maintenanceModeEnabled")
.notNull() .notNull()
.default(false), .default(false),
@@ -175,15 +159,7 @@ export const resources = pgTable("resources", {
maintenanceEstimatedTime: text("maintenanceEstimatedTime"), maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
postAuthPath: text("postAuthPath"), postAuthPath: text("postAuthPath"),
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown" health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: boolean("wildcard").notNull().default(false), wildcard: boolean("wildcard").notNull().default(false)
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
pamMode: varchar("pamMode", { length: 32 })
.$type<"passthrough" | "push">()
.default("passthrough"),
authDaemonMode: varchar("authDaemonMode", { length: 32 })
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123)
}); });
export const labels = pgTable("labels", { export const labels = pgTable("labels", {
@@ -363,11 +339,11 @@ export const siteResources = pgTable("siteResources", {
niceId: varchar("niceId").notNull(), niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(), name: varchar("name").notNull(),
ssl: boolean("ssl").notNull().default(false), ssl: boolean("ssl").notNull().default(false),
mode: varchar("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http" mode: varchar("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode destinationPort: integer("destinationPort"), // only for port mode
destination: varchar("destination"), // ip, cidr, hostname; validate against the mode destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
enabled: boolean("enabled").notNull().default(true), enabled: boolean("enabled").notNull().default(true),
alias: varchar("alias"), alias: varchar("alias"),
aliasAddress: varchar("aliasAddress"), aliasAddress: varchar("aliasAddress"),
@@ -375,11 +351,8 @@ export const siteResources = pgTable("siteResources", {
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"), udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
disableIcmp: boolean("disableIcmp").notNull().default(false), disableIcmp: boolean("disableIcmp").notNull().default(false),
authDaemonPort: integer("authDaemonPort").default(22123), authDaemonPort: integer("authDaemonPort").default(22123),
pamMode: varchar("pamMode", { length: 32 })
.$type<"passthrough" | "push">()
.default("passthrough"),
authDaemonMode: varchar("authDaemonMode", { length: 32 }) authDaemonMode: varchar("authDaemonMode", { length: 32 })
.$type<"site" | "remote" | "native">() .$type<"site" | "remote">()
.default("site"), .default("site"),
domainId: varchar("domainId").references(() => domains.domainId, { domainId: varchar("domainId").references(() => domains.domainId, {
onDelete: "set null" onDelete: "set null"
@@ -633,38 +606,6 @@ export const userResources = pgTable("userResources", {
.references(() => resources.resourceId, { onDelete: "cascade" }) .references(() => resources.resourceId, { onDelete: "cascade" })
}); });
export const rolePolicies = pgTable("rolePolicies", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const userPolicies = pgTable("userPolicies", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const resourcePolicyWhiteList = pgTable("resourcePolicyWhitelist", {
whitelistId: serial("id").primaryKey(),
email: varchar("email").notNull(),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const userInvites = pgTable("userInvites", { export const userInvites = pgTable("userInvites", {
inviteId: varchar("inviteId").primaryKey(), inviteId: varchar("inviteId").primaryKey(),
orgId: varchar("orgId") orgId: varchar("orgId")
@@ -730,40 +671,6 @@ export const resourceHeaderAuthExtendedCompatibility = pgTable(
} }
); );
export const resourcePolicyPincode = pgTable("resourcePolicyPincode", {
pincodeId: serial("pincodeId").primaryKey(),
pincodeHash: varchar("pincodeHash").notNull(),
digitLength: integer("digitLength").notNull(),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const resourcePolicyPassword = pgTable("resourcePolicyPassword", {
passwordId: serial("passwordId").primaryKey(),
passwordHash: varchar("passwordHash").notNull(),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const resourcePolicyHeaderAuth = pgTable("resourcePolicyHeaderAuth", {
headerAuthId: serial("headerAuthId").primaryKey(),
headerAuthHash: varchar("headerAuthHash").notNull(),
extendedCompatibility: boolean("extendedCompatibility")
.notNull()
.default(true),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const resourceAccessToken = pgTable("resourceAccessToken", { export const resourceAccessToken = pgTable("resourceAccessToken", {
accessTokenId: varchar("accessTokenId").primaryKey(), accessTokenId: varchar("accessTokenId").primaryKey(),
orgId: varchar("orgId") orgId: varchar("orgId")
@@ -772,7 +679,6 @@ export const resourceAccessToken = pgTable("resourceAccessToken", {
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
path: varchar("path"),
tokenHash: varchar("tokenHash").notNull(), tokenHash: varchar("tokenHash").notNull(),
sessionLength: bigint("sessionLength", { mode: "number" }).notNull(), sessionLength: bigint("sessionLength", { mode: "number" }).notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }), expiresAt: bigint("expiresAt", { mode: "number" }),
@@ -820,24 +726,6 @@ export const resourceSessions = pgTable("resourceSessions", {
onDelete: "cascade" onDelete: "cascade"
} }
), ),
policyPasswordId: integer("policyPasswordId").references(
() => resourcePolicyPassword.passwordId,
{
onDelete: "cascade"
}
),
policyPincodeId: integer("policyPincodeId").references(
() => resourcePolicyPincode.pincodeId,
{
onDelete: "cascade"
}
),
policyWhitelistId: integer("policyWhitelistId").references(
() => resourcePolicyWhiteList.whitelistId,
{
onDelete: "cascade"
}
),
issuedAt: bigint("issuedAt", { mode: "number" }) issuedAt: bigint("issuedAt", { mode: "number" })
}); });
@@ -876,43 +764,6 @@ export const resourceRules = pgTable("resourceRules", {
value: varchar("value").notNull() value: varchar("value").notNull()
}); });
export const resourcePolicyRules = pgTable("resourcePolicyRules", {
ruleId: serial("ruleId").primaryKey(),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
}),
enabled: boolean("enabled").notNull().default(true),
priority: integer("priority").notNull(),
action: varchar("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
match: varchar("match").$type<"CIDR" | "PATH" | "IP">().notNull(),
value: varchar("value").notNull()
});
export const resourcePolicies = pgTable("resourcePolicies", {
resourcePolicyId: serial("resourcePolicyId").primaryKey(),
sso: boolean("sso").notNull().default(true),
applyRules: boolean("applyRules").notNull().default(false),
scope: varchar("scope")
.$type<"global" | "resource">()
.notNull()
.default("global"),
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
.notNull()
.default(false),
idpId: integer("idpId").references(() => idp.idpId, {
onDelete: "set null"
}),
niceId: text("niceId").notNull(),
name: varchar("name").notNull(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull()
});
export const supporterKey = pgTable("supporterKey", { export const supporterKey = pgTable("supporterKey", {
keyId: serial("keyId").primaryKey(), keyId: serial("keyId").primaryKey(),
key: varchar("key").notNull(), key: varchar("key").notNull(),
@@ -1425,6 +1276,3 @@ export type RoundTripMessageTracker = InferSelectModel<
export type Network = InferSelectModel<typeof networks>; export type Network = InferSelectModel<typeof networks>;
export type StatusHistory = InferSelectModel<typeof statusHistory>; export type StatusHistory = InferSelectModel<typeof statusHistory>;
export type Label = InferSelectModel<typeof labels>; export type Label = InferSelectModel<typeof labels>;
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
export type UserPolicy = InferSelectModel<typeof userPolicies>;
+36 -146
View File
@@ -17,31 +17,21 @@ import {
resourceHeaderAuth, resourceHeaderAuth,
ResourceHeaderAuth, ResourceHeaderAuth,
resourceRules, resourceRules,
resourcePolicyRules,
resources, resources,
roleResources, roleResources,
rolePolicies,
sessions, sessions,
userResources, userResources,
userPolicies,
users, users,
ResourceHeaderAuthExtendedCompatibility, ResourceHeaderAuthExtendedCompatibility,
resourceHeaderAuthExtendedCompatibility, resourceHeaderAuthExtendedCompatibility
resourcePolicies,
resourcePolicyPincode,
ResourcePolicyPincode,
resourcePolicyPassword,
ResourcePolicyPassword,
resourcePolicyHeaderAuth,
ResourcePolicyHeaderAuth
} from "@server/db"; } from "@server/db";
import { and, eq, inArray, or, sql } from "drizzle-orm"; import { and, eq, inArray, or, sql } from "drizzle-orm";
export type ResourceWithAuth = { export type ResourceWithAuth = {
resource: Resource | null; resource: Resource | null;
pincode: ResourcePincode | ResourcePolicyPincode | null; pincode: ResourcePincode | null;
password: ResourcePassword | ResourcePolicyPassword | null; password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | ResourcePolicyHeaderAuth | null; headerAuth: ResourceHeaderAuth | null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
org: Org; org: Org;
}; };
@@ -89,31 +79,6 @@ export async function getResourceByDomain(
resources.resourceId resources.resourceId
) )
) )
.leftJoin(
resourcePolicies,
eq(resourcePolicies.resourcePolicyId, resources.resourcePolicyId)
)
.leftJoin(
resourcePolicyPincode,
eq(
resourcePolicyPincode.resourcePolicyId,
resourcePolicies.resourcePolicyId
)
)
.leftJoin(
resourcePolicyPassword,
eq(
resourcePolicyPassword.resourcePolicyId,
resourcePolicies.resourcePolicyId
)
)
.leftJoin(
resourcePolicyHeaderAuth,
eq(
resourcePolicyHeaderAuth.resourcePolicyId,
resourcePolicies.resourcePolicyId
)
)
.innerJoin(orgs, eq(orgs.orgId, resources.orgId)) .innerJoin(orgs, eq(orgs.orgId, resources.orgId))
.where( .where(
or( or(
@@ -145,18 +110,11 @@ export async function getResourceByDomain(
return { return {
resource: result.resources, resource: result.resources,
pincode: result.resourcePolicyPincode ?? result.resourcePincode, pincode: result.resourcePincode,
password: result.resourcePolicyPassword ?? result.resourcePassword, password: result.resourcePassword,
headerAuth: headerAuth: result.resourceHeaderAuth,
result.resourcePolicyHeaderAuth ?? result.resourceHeaderAuth, headerAuthExtendedCompatibility:
headerAuthExtendedCompatibility: result.resourcePolicyHeaderAuth result.resourceHeaderAuthExtendedCompatibility,
? ({
headerAuthExtendedCompatibilityId: 0,
resourceId: result.resources.resourceId,
extendedCompatibilityIsActivated:
result.resourcePolicyHeaderAuth.extendedCompatibility
} as ResourceHeaderAuthExtendedCompatibility)
: result.resourceHeaderAuthExtendedCompatibility,
org: result.orgs org: result.orgs
}; };
} }
@@ -196,126 +154,58 @@ export async function getRoleName(roleId: number): Promise<string | null> {
} }
/** /**
* Check if role has access to resource (direct or via resource policy) * Check if role has access to resource
*/ */
export async function getRoleResourceAccess( export async function getRoleResourceAccess(
resourceId: number, resourceId: number,
roleIds: number[] roleIds: number[]
) { ) {
const [direct, viaPolicies] = await Promise.all([ const roleResourceAccess = await db
db .select()
.select() .from(roleResources)
.from(roleResources) .where(
.where( and(
and( eq(roleResources.resourceId, resourceId),
eq(roleResources.resourceId, resourceId), inArray(roleResources.roleId, roleIds)
inArray(roleResources.roleId, roleIds)
)
),
db
.select({
roleId: rolePolicies.roleId,
resourcePolicyId: rolePolicies.resourcePolicyId
})
.from(rolePolicies)
.innerJoin(
resources,
eq(resources.resourcePolicyId, rolePolicies.resourcePolicyId)
) )
.where( );
and(
eq(resources.resourceId, resourceId),
inArray(rolePolicies.roleId, roleIds)
)
)
]);
const combined = [...direct, ...viaPolicies]; return roleResourceAccess.length > 0 ? roleResourceAccess : null;
return combined.length > 0 ? combined : null;
} }
/** /**
* Check if user has access to resource (direct or via resource policy) * Check if user has direct access to resource
*/ */
export async function getUserResourceAccess( export async function getUserResourceAccess(
userId: string, userId: string,
resourceId: number resourceId: number
) { ) {
const [direct, viaPolicies] = await Promise.all([ const userResourceAccess = await db
db .select()
.select() .from(userResources)
.from(userResources) .where(
.where( and(
and( eq(userResources.userId, userId),
eq(userResources.userId, userId), eq(userResources.resourceId, resourceId)
eq(userResources.resourceId, resourceId)
)
) )
.limit(1), )
db .limit(1);
.select({
userId: userPolicies.userId,
resourcePolicyId: userPolicies.resourcePolicyId
})
.from(userPolicies)
.innerJoin(
resources,
eq(resources.resourcePolicyId, userPolicies.resourcePolicyId)
)
.where(
and(
eq(resources.resourceId, resourceId),
eq(userPolicies.userId, userId)
)
)
.limit(1)
]);
return direct[0] ?? viaPolicies[0] ?? null; return userResourceAccess.length > 0 ? userResourceAccess[0] : null;
} }
/** /**
* Get resource rules for a given resource (direct and via resource policy) * Get resource rules for a given resource
*/ */
export async function getResourceRules( export async function getResourceRules(
resourceId: number resourceId: number
): Promise<ResourceRule[]> { ): Promise<ResourceRule[]> {
const [directRules, policyRules] = await Promise.all([ const rules = await db
db .select()
.select() .from(resourceRules)
.from(resourceRules) .where(eq(resourceRules.resourceId, resourceId));
.where(eq(resourceRules.resourceId, resourceId)),
db
.select({
ruleId: resourcePolicyRules.ruleId,
resourceId: sql<number>`${resourceId}`,
enabled: resourcePolicyRules.enabled,
priority: resourcePolicyRules.priority,
action: resourcePolicyRules.action,
match: resourcePolicyRules.match,
value: resourcePolicyRules.value
})
.from(resourcePolicyRules)
.innerJoin(
resources,
eq(
resources.resourcePolicyId,
resourcePolicyRules.resourcePolicyId
)
)
.where(eq(resources.resourceId, resourceId))
]);
const maxDirectPriority = directRules.reduce( return rules;
(max, r) => Math.max(max, r.priority),
0
);
const offsetPolicyRules = policyRules.map((r) => ({
...r,
priority: maxDirectPriority + r.priority
}));
return [...directRules, ...offsetPolicyRules] as ResourceRule[];
} }
/** /**
-23
View File
@@ -588,26 +588,6 @@ export const trialNotifications = sqliteTable("trialNotifications", {
sentAt: integer("sentAt").notNull() sentAt: integer("sentAt").notNull()
}); });
export const browserGatewayTarget = sqliteTable("browserGatewayTarget", {
browserGatewayTargetId: integer("browserGatewayTargetId").primaryKey({
autoIncrement: true
}),
resourceId: integer("resourceId")
.references(() => resources.resourceId, {
onDelete: "cascade"
})
.notNull(),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
})
.notNull(),
authToken: text("authToken").notNull(),
type: text("type").notNull(), // "ssh", "rdp", "vnc"
destination: text("destination").notNull(),
destinationPort: integer("destinationPort").notNull()
});
export type Approval = InferSelectModel<typeof approvals>; export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>; export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>; export type Account = InferSelectModel<typeof account>;
@@ -647,6 +627,3 @@ export type AlertEmailAction = InferSelectModel<typeof alertEmailActions>;
export type AlertEmailRecipient = InferSelectModel<typeof alertEmailRecipients>; export type AlertEmailRecipient = InferSelectModel<typeof alertEmailRecipients>;
export type AlertWebhookAction = InferSelectModel<typeof alertWebhookActions>; export type AlertWebhookAction = InferSelectModel<typeof alertWebhookActions>;
export type TrialNotification = InferSelectModel<typeof trialNotifications>; export type TrialNotification = InferSelectModel<typeof trialNotifications>;
export type BrowserGatewayTarget = InferSelectModel<
typeof browserGatewayTarget
>;
+8 -183
View File
@@ -62,13 +62,7 @@ export const orgs = sqliteTable("orgs", {
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format) sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format) sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }), isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
billingOrgId: text("billingOrgId"), billingOrgId: text("billingOrgId")
settingsEnableGlobalNewtAutoUpdate: integer(
"settingsEnableGlobalNewtAutoUpdate",
{ mode: "boolean" }
)
.notNull()
.default(false)
}); });
export const userDomains = sqliteTable("userDomains", { export const userDomains = sqliteTable("userDomains", {
@@ -122,29 +116,11 @@ export const sites = sqliteTable("sites", {
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
.notNull() .notNull()
.default(true), .default(true),
autoUpdateEnabled: integer("autoUpdateEnabled", { mode: "boolean" })
.notNull()
.default(false),
autoUpdateOverrideOrg: integer("autoUpdateOverrideOrg", {
mode: "boolean"
})
.notNull()
.default(false),
status: text("status").$type<"pending" | "approved">().default("approved") status: text("status").$type<"pending" | "approved">().default("approved")
}); });
export const resources = sqliteTable("resources", { export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
resourcePolicyId: integer("resourcePolicyId").references(
() => resourcePolicies.resourcePolicyId,
{ onDelete: "set null" }
),
defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
() => resourcePolicies.resourcePolicyId,
{
onDelete: "restrict"
}
),
resourceGuid: text("resourceGuid", { length: 36 }) resourceGuid: text("resourceGuid", { length: 36 })
.unique() .unique()
.notNull() .notNull()
@@ -166,6 +142,8 @@ export const resources = sqliteTable("resources", {
.notNull() .notNull()
.default(false), .default(false),
sso: integer("sso", { mode: "boolean" }).notNull().default(true), sso: integer("sso", { mode: "boolean" }).notNull().default(true),
http: integer("http", { mode: "boolean" }).notNull().default(true),
protocol: text("protocol").notNull(),
proxyPort: integer("proxyPort"), proxyPort: integer("proxyPort"),
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
.notNull() .notNull()
@@ -188,6 +166,7 @@ export const resources = sqliteTable("resources", {
.notNull() .notNull()
.default(false), .default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1), proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
maintenanceModeEnabled: integer("maintenanceModeEnabled", { maintenanceModeEnabled: integer("maintenanceModeEnabled", {
mode: "boolean" mode: "boolean"
}) })
@@ -201,15 +180,7 @@ export const resources = sqliteTable("resources", {
maintenanceEstimatedTime: text("maintenanceEstimatedTime"), maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
postAuthPath: text("postAuthPath"), postAuthPath: text("postAuthPath"),
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown" health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false), wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false)
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
pamMode: text("pamMode")
.$type<"passthrough" | "push">()
.default("passthrough"),
authDaemonMode: text("authDaemonMode")
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123)
}); });
export const labels = sqliteTable("labels", { export const labels = sqliteTable("labels", {
@@ -401,11 +372,11 @@ export const siteResources = sqliteTable("siteResources", {
niceId: text("niceId").notNull(), niceId: text("niceId").notNull(),
name: text("name").notNull(), name: text("name").notNull(),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false), ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
mode: text("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http" mode: text("mode").$type<"host" | "cidr" | "http">().notNull(), // "host" | "cidr" | "http"
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode proxyPort: integer("proxyPort"), // only for port mode
destinationPort: integer("destinationPort"), // only for port mode destinationPort: integer("destinationPort"), // only for port mode
destination: text("destination"), // ip, cidr, hostname destination: text("destination").notNull(), // ip, cidr, hostname
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
alias: text("alias"), alias: text("alias"),
aliasAddress: text("aliasAddress"), aliasAddress: text("aliasAddress"),
@@ -415,11 +386,8 @@ export const siteResources = sqliteTable("siteResources", {
.notNull() .notNull()
.default(false), .default(false),
authDaemonPort: integer("authDaemonPort").default(22123), authDaemonPort: integer("authDaemonPort").default(22123),
pamMode: text("pamMode")
.$type<"passthrough" | "push">()
.default("passthrough"),
authDaemonMode: text("authDaemonMode") authDaemonMode: text("authDaemonMode")
.$type<"site" | "remote" | "native">() .$type<"site" | "remote">()
.default("site"), .default("site"),
domainId: text("domainId").references(() => domains.domainId, { domainId: text("domainId").references(() => domains.domainId, {
onDelete: "set null" onDelete: "set null"
@@ -1032,47 +1000,6 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
headerAuthHash: text("headerAuthHash").notNull() headerAuthHash: text("headerAuthHash").notNull()
}); });
export const resourcePolicyPincode = sqliteTable("resourcePolicyPincode", {
pincodeId: integer("pincodeId").primaryKey({ autoIncrement: true }),
pincodeHash: text("pincodeHash").notNull(),
digitLength: integer("digitLength").notNull(),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const resourcePolicyPassword = sqliteTable("resourcePolicyPassword", {
passwordId: integer("passwordId").primaryKey({ autoIncrement: true }),
passwordHash: text("passwordHash").notNull(),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const resourcePolicyHeaderAuth = sqliteTable(
"resourcePolicyHeaderAuth",
{
headerAuthId: integer("headerAuthId").primaryKey({
autoIncrement: true
}),
headerAuthHash: text("headerAuthHash").notNull(),
extendedCompatibility: integer("extendedCompatibility", {
mode: "boolean"
})
.notNull()
.default(true),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
}
);
export const resourceHeaderAuthExtendedCompatibility = sqliteTable( export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
"resourceHeaderAuthExtendedCompatibility", "resourceHeaderAuthExtendedCompatibility",
{ {
@@ -1101,7 +1028,6 @@ export const resourceAccessToken = sqliteTable("resourceAccessToken", {
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, { onDelete: "cascade" }),
path: text("path"),
tokenHash: text("tokenHash").notNull(), tokenHash: text("tokenHash").notNull(),
sessionLength: integer("sessionLength").notNull(), sessionLength: integer("sessionLength").notNull(),
expiresAt: integer("expiresAt"), expiresAt: integer("expiresAt"),
@@ -1148,24 +1074,6 @@ export const resourceSessions = sqliteTable("resourceSessions", {
onDelete: "cascade" onDelete: "cascade"
} }
), ),
policyPasswordId: integer("policyPasswordId").references(
() => resourcePolicyPassword.passwordId,
{
onDelete: "cascade"
}
),
policyPincodeId: integer("policyPincodeId").references(
() => resourcePolicyPincode.pincodeId,
{
onDelete: "cascade"
}
),
policyWhitelistId: integer("policyWhitelistId").references(
() => resourcePolicyWhiteList.whitelistId,
{
onDelete: "cascade"
}
),
issuedAt: integer("issuedAt") issuedAt: integer("issuedAt")
}); });
@@ -1206,77 +1114,6 @@ export const resourceRules = sqliteTable("resourceRules", {
value: text("value").notNull() value: text("value").notNull()
}); });
export const rolePolicies = sqliteTable("rolePolicies", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const userPolicies = sqliteTable("userPolicies", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const resourcePolicyWhiteList = sqliteTable("resourcePolicyWhitelist", {
whitelistId: integer("id").primaryKey({ autoIncrement: true }),
email: text("email").notNull(),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
})
});
export const resourcePolicyRules = sqliteTable("resourcePolicyRules", {
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }),
resourcePolicyId: integer("resourcePolicyId")
.notNull()
.references(() => resourcePolicies.resourcePolicyId, {
onDelete: "cascade"
}),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
priority: integer("priority").notNull(),
action: text("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(),
match: text("match").$type<"CIDR" | "PATH" | "IP">().notNull(),
value: text("value").notNull()
});
export const resourcePolicies = sqliteTable("resourcePolicies", {
resourcePolicyId: integer("resourcePolicyId").primaryKey(),
sso: integer("sso", { mode: "boolean" }).notNull().default(true),
applyRules: integer("applyRules", { mode: "boolean" })
.notNull()
.default(false),
scope: text("scope")
.$type<"global" | "resource">()
.notNull()
.default("global"),
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
.notNull()
.default(false),
niceId: text("niceId").notNull(),
idpId: integer("idpId").references(() => idp.idpId, {
onDelete: "set null"
}),
name: text("name").notNull(),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull()
});
export const supporterKey = sqliteTable("supporterKey", { export const supporterKey = sqliteTable("supporterKey", {
keyId: integer("keyId").primaryKey({ autoIncrement: true }), keyId: integer("keyId").primaryKey({ autoIncrement: true }),
key: text("key").notNull(), key: text("key").notNull(),
@@ -1544,15 +1381,3 @@ export type RoundTripMessageTracker = InferSelectModel<
>; >;
export type StatusHistory = InferSelectModel<typeof statusHistory>; export type StatusHistory = InferSelectModel<typeof statusHistory>;
export type Label = InferSelectModel<typeof labels>; export type Label = InferSelectModel<typeof labels>;
export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
export type ResourcePolicyPincode = InferSelectModel<
typeof resourcePolicyPincode
>;
export type ResourcePolicyPassword = InferSelectModel<
typeof resourcePolicyPassword
>;
export type ResourcePolicyHeaderAuth = InferSelectModel<
typeof resourcePolicyHeaderAuth
>;
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
export type UserPolicy = InferSelectModel<typeof userPolicies>;
+3 -9
View File
@@ -152,17 +152,11 @@ function getOpenApiDocumentation() {
if (!hasExistingResponses) { if (!hasExistingResponses) {
def.route.responses = { def.route.responses = {
"200": { "*": {
description: "Successful response", description: "",
content: { content: {
"application/json": { "application/json": {
schema: z.object({ schema: z.object({})
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
} }
} }
} }
+3 -11
View File
@@ -221,18 +221,10 @@ async function handleResource(
) )
.where(eq(targets.resourceId, resource.resourceId)); .where(eq(targets.resourceId, resource.resourceId));
const monitoredTargets = otherTargets.filter(
(t) => t.hcHealth !== "unknown"
);
let health = "healthy"; let health = "healthy";
const allUnknown = monitoredTargets.length === 0; const allUnknown = otherTargets.every((t) => t.hcHealth === "unknown");
const allHealthy = monitoredTargets.every( const allHealthy = otherTargets.every((t) => t.hcHealth === "healthy");
(t) => t.hcHealth === "healthy" const allUnhealthy = otherTargets.every((t) => t.hcHealth === "unhealthy");
);
const allUnhealthy = monitoredTargets.every(
(t) => t.hcHealth === "unhealthy"
);
if (allUnknown) { if (allUnknown) {
logger.debug( logger.debug(
+6 -20
View File
@@ -16,18 +16,16 @@ export enum TierFeature {
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
SshPam = "sshPam",
FullRbac = "fullRbac", FullRbac = "fullRbac",
SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed
SIEM = "siem", // handle downgrade by disabling SIEM integrations SIEM = "siem", // handle downgrade by disabling SIEM integrations
HTTPPrivateResources = "httpPrivateResources", // handle downgrade by disabling HTTP private resources
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
StandaloneHealthChecks = "standaloneHealthChecks", StandaloneHealthChecks = "standaloneHealthChecks",
AlertingRules = "alertingRules", AlertingRules = "alertingRules",
WildcardSubdomain = "wildcardSubdomain", WildcardSubdomain = "wildcardSubdomain",
Labels = "labels", Labels = "labels"
NewtAutoUpdate = "newtAutoUpdate",
ResourcePolicies = "resourcePolicies",
AdvancedPublicResources = "advancedPublicResources",
AdvancedPrivateResources = "advancedPrivateResources"
} }
export const tierMatrix: Record<TierFeature, Tier[]> = { export const tierMatrix: Record<TierFeature, Tier[]> = {
@@ -62,25 +60,13 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
"enterprise" "enterprise"
], ],
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"], [TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"], [TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
[TierFeature.SIEM]: ["enterprise"], [TierFeature.SIEM]: ["enterprise"],
[TierFeature.HTTPPrivateResources]: ["tier3", "enterprise"],
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"], [TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
[TierFeature.AlertingRules]: ["tier3", "enterprise"], [TierFeature.AlertingRules]: ["tier3", "enterprise"],
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"]
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"],
[TierFeature.AdvancedPublicResources]: [
"tier1",
"tier2",
"tier3",
"enterprise"
],
[TierFeature.AdvancedPrivateResources]: [
"tier1",
"tier2",
"tier3",
"enterprise"
]
}; };
+6 -3
View File
@@ -16,13 +16,14 @@ import logger from "@server/logger";
import { sites } from "@server/db"; import { sites } from "@server/db";
import { eq, and, isNotNull } from "drizzle-orm"; import { eq, and, isNotNull } from "drizzle-orm";
import { addTargets as addProxyTargets } from "@server/routers/newt/targets"; import { addTargets as addProxyTargets } from "@server/routers/newt/targets";
import { addTargets as addClientTargets } from "@server/routers/client/targets";
import { import {
ClientResourcesResults, ClientResourcesResults,
updateClientResources updateClientResources
} from "./clientResources"; } from "./clientResources";
import { BlueprintSource } from "@server/routers/blueprints/types"; import { BlueprintSource } from "@server/routers/blueprints/types";
import { stringify as stringifyYaml } from "yaml"; import { stringify as stringifyYaml } from "yaml";
import { generateName } from "@server/db/names"; import { faker } from "@faker-js/faker";
import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource"; import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations"; import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations";
@@ -105,7 +106,7 @@ export async function applyBlueprint({
site.newt.newtId, site.newt.newtId,
[target], [target],
matchingHealthcheck ? [matchingHealthcheck] : [], matchingHealthcheck ? [matchingHealthcheck] : [],
result.proxyResource.mode === "udp" ? "udp" : "tcp", result.proxyResource.protocol,
site.newt.version site.newt.version
); );
} }
@@ -290,7 +291,9 @@ export async function applyBlueprint({
.insert(blueprints) .insert(blueprints)
.values({ .values({
orgId, orgId,
name: name ?? generateName(), name:
name ??
`${faker.word.adjective()}-${faker.word.adjective()}-${faker.word.noun()}`,
contents: stringifyYaml(configData), contents: stringifyYaml(configData),
createdAt: Math.floor(Date.now() / 1000), createdAt: Math.floor(Date.now() / 1000),
succeeded: blueprintSucceeded, succeeded: blueprintSucceeded,
@@ -1,56 +1,10 @@
import { sendToClient } from "#dynamic/routers/ws"; import { sendToClient } from "#dynamic/routers/ws";
import { processContainerLabels } from "./parseDockerContainers"; import { processContainerLabels } from "./parseDockerContainers";
import { applyBlueprint } from "./applyBlueprint"; import { applyBlueprint } from "./applyBlueprint";
import { PrivateResourceSchema, PublicResourceSchema } from "./types";
import { db, sites } from "@server/db"; import { db, sites } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
type BlueprintResult = ReturnType<typeof processContainerLabels>;
function filterInvalidResources(blueprint: BlueprintResult): {
skippedCount: number;
skippedKeys: string[];
} {
const skippedKeys: string[] = [];
for (const section of ["proxy-resources", "public-resources"] as const) {
const resources = blueprint[section];
for (const [key, value] of Object.entries(resources)) {
const result = PublicResourceSchema.safeParse(value);
if (!result.success) {
const errors = result.error.issues
.map((i) => `${i.path.join(".")}: ${i.message}`)
.join("; ");
logger.warn(
`Skipping invalid Docker ${section} "${key}": ${errors}`
);
delete resources[key];
skippedKeys.push(`${section}.${key}`);
}
}
}
for (const section of ["client-resources", "private-resources"] as const) {
const resources = blueprint[section];
for (const [key, value] of Object.entries(resources)) {
const result = PrivateResourceSchema.safeParse(value);
if (!result.success) {
const errors = result.error.issues
.map((i) => `${i.path.join(".")}: ${i.message}`)
.join("; ");
logger.warn(
`Skipping invalid Docker ${section} "${key}": ${errors}`
);
delete resources[key];
skippedKeys.push(`${section}.${key}`);
}
}
}
return { skippedCount: skippedKeys.length, skippedKeys };
}
export async function applyNewtDockerBlueprint( export async function applyNewtDockerBlueprint(
siteId: number, siteId: number,
newtId: string, newtId: string,
@@ -67,24 +21,17 @@ export async function applyNewtDockerBlueprint(
return; return;
} }
let skippedCount = 0; // logger.debug(`Applying Docker blueprint to site: ${siteId}`);
let skippedKeys: string[] = []; // logger.debug(`Containers: ${JSON.stringify(containers, null, 2)}`);
try { try {
const blueprint = processContainerLabels(containers); const blueprint = processContainerLabels(containers);
logger.debug( logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`);
`Received Docker blueprint with ${Object.keys(blueprint["proxy-resources"]).length} proxy, ${Object.keys(blueprint["client-resources"]).length} client resource(s)`
);
const filterResult = filterInvalidResources(blueprint); // make sure this is not an empty object
skippedCount = filterResult.skippedCount; if (isEmptyObject(blueprint)) {
skippedKeys = filterResult.skippedKeys; return;
if (skippedCount > 0) {
logger.warn(
`Filtered ${skippedCount} invalid resource(s) from Docker blueprint: ${skippedKeys.join(", ")}`
);
} }
if ( if (
@@ -93,15 +40,6 @@ export async function applyNewtDockerBlueprint(
isEmptyObject(blueprint["public-resources"]) && isEmptyObject(blueprint["public-resources"]) &&
isEmptyObject(blueprint["private-resources"]) isEmptyObject(blueprint["private-resources"])
) { ) {
if (skippedCount > 0) {
await sendToClient(newtId, {
type: "newt/blueprint/results",
data: {
success: false,
message: `All resources were invalid and skipped: ${skippedKeys.join(", ")}`
}
});
}
return; return;
} }
@@ -128,10 +66,7 @@ export async function applyNewtDockerBlueprint(
type: "newt/blueprint/results", type: "newt/blueprint/results",
data: { data: {
success: true, success: true,
message: message: "Config updated successfully"
skippedCount > 0
? `Config updated successfully. Skipped ${skippedCount} invalid resource(s): ${skippedKeys.join(", ")}`
: "Config updated successfully"
} }
}); });
} }
+9 -67
View File
@@ -3,7 +3,6 @@ import {
clientSiteResources, clientSiteResources,
domains, domains,
orgDomains, orgDomains,
roleActions,
roles, roles,
roleSiteResources, roleSiteResources,
Site, Site,
@@ -20,7 +19,6 @@ import { sites } from "@server/db";
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm"; import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
import { Config } from "./types"; import { Config } from "./types";
import logger from "@server/logger"; import logger from "@server/logger";
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
import { getNextAvailableAliasAddress } from "../ip"; import { getNextAvailableAliasAddress } from "../ip";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
@@ -227,11 +225,7 @@ export async function updateClientResources(
: resourceData["udp-ports"], : resourceData["udp-ports"],
fullDomain: resourceData["full-domain"] || null, fullDomain: resourceData["full-domain"] || null,
subdomain: domainInfo ? domainInfo.subdomain : null, subdomain: domainInfo ? domainInfo.subdomain : null,
domainId: domainInfo ? domainInfo.domainId : null, domainId: domainInfo ? domainInfo.domainId : null
pamMode: resourceData["auth-daemon"]?.pam || "passthrough",
authDaemonMode:
resourceData["auth-daemon"]?.mode || "native",
authDaemonPort: resourceData["auth-daemon"]?.port || 22123
}) })
.where( .where(
eq( eq(
@@ -338,7 +332,8 @@ export async function updateClientResources(
} }
if (resourceData.roles.length > 0) { if (resourceData.roles.length > 0) {
const existingRoles = await trx // Re-add specified roles but we need to get the roleIds from the role name in the array
const rolesToUpdate = await trx
.select() .select()
.from(roles) .from(roles)
.where( .where(
@@ -348,28 +343,7 @@ export async function updateClientResources(
) )
); );
const foundNames = new Set(existingRoles.map((r) => r.name)); const roleIds = rolesToUpdate.map((role) => role.roleId);
const missingNames = resourceData.roles.filter(
(n) => !foundNames.has(n)
);
for (const name of missingNames) {
const [created] = await trx
.insert(roles)
.values({ name, orgId })
.returning();
await trx.insert(roleActions).values(
defaultRoleAllowedActions.map((action) => ({
roleId: created.roleId,
actionId: action,
orgId
}))
);
existingRoles.push(created);
logger.info(`Auto-created role "${name}" in org ${orgId} from blueprint`);
}
const roleIds = existingRoles.map((role) => role.roleId);
await trx await trx
.insert(roleSiteResources) .insert(roleSiteResources)
@@ -386,14 +360,8 @@ export async function updateClientResources(
}); });
} else { } else {
let aliasAddress: string | null = null; let aliasAddress: string | null = null;
let releaseAliasLock: (() => Promise<void>) | null = null;
if (resourceData.mode === "host" || resourceData.mode === "http") { if (resourceData.mode === "host" || resourceData.mode === "http") {
const { value, release } = await getNextAvailableAliasAddress( aliasAddress = await getNextAvailableAliasAddress(orgId, trx);
orgId,
trx
);
aliasAddress = value;
releaseAliasLock = release;
} }
let domainInfo: let domainInfo:
@@ -447,16 +415,10 @@ export async function updateClientResources(
: resourceData["udp-ports"], : resourceData["udp-ports"],
fullDomain: resourceData["full-domain"] || null, fullDomain: resourceData["full-domain"] || null,
subdomain: domainInfo ? domainInfo.subdomain : null, subdomain: domainInfo ? domainInfo.subdomain : null,
domainId: domainInfo ? domainInfo.domainId : null, domainId: domainInfo ? domainInfo.domainId : null
pamMode: resourceData["auth-daemon"]?.pam || "passthrough",
authDaemonMode:
resourceData["auth-daemon"]?.mode || "native",
authDaemonPort: resourceData["auth-daemon"]?.port || 22123
}) })
.returning(); .returning();
await releaseAliasLock?.();
const siteResourceId = newResource.siteResourceId; const siteResourceId = newResource.siteResourceId;
for (const site of allSites) { for (const site of allSites) {
@@ -482,7 +444,8 @@ export async function updateClientResources(
}); });
if (resourceData.roles.length > 0) { if (resourceData.roles.length > 0) {
const existingRoles = await trx // get roleIds from role names
const rolesToUpdate = await trx
.select() .select()
.from(roles) .from(roles)
.where( .where(
@@ -492,28 +455,7 @@ export async function updateClientResources(
) )
); );
const foundNames = new Set(existingRoles.map((r) => r.name)); const roleIds = rolesToUpdate.map((role) => role.roleId);
const missingNames = resourceData.roles.filter(
(n) => !foundNames.has(n)
);
for (const name of missingNames) {
const [created] = await trx
.insert(roles)
.values({ name, orgId })
.returning();
await trx.insert(roleActions).values(
defaultRoleAllowedActions.map((action) => ({
roleId: created.roleId,
actionId: action,
orgId
}))
);
existingRoles.push(created);
logger.info(`Auto-created role "${name}" in org ${orgId} from blueprint`);
}
const roleIds = existingRoles.map((role) => role.roleId);
await trx await trx
.insert(roleSiteResources) .insert(roleSiteResources)
File diff suppressed because it is too large Load Diff
+29 -108
View File
@@ -82,7 +82,7 @@ export const RuleSchema = z
.object({ .object({
action: z.enum(["allow", "deny", "pass"]), action: z.enum(["allow", "deny", "pass"]),
match: z.enum(["cidr", "path", "ip", "country", "asn", "region"]), match: z.enum(["cidr", "path", "ip", "country", "asn", "region"]),
value: z.coerce.string(), value: z.string(),
priority: z.int().optional() priority: z.int().optional()
}) })
.refine( .refine(
@@ -161,34 +161,11 @@ export const HeaderSchema = z.object({
value: z.string().min(1) value: z.string().min(1)
}); });
export const AuthDaemonSchema = z
.object({
pam: z.enum(["passthrough", "push"]).optional().default("passthrough"),
mode: z.enum(["site", "remote", "native"]).optional().default("site"),
port: z.int().min(1).max(65535).optional()
})
.refine(
(data) => {
if (data.mode === "remote") {
return data.port !== undefined;
}
return true;
},
{
path: ["port"],
message: "port is required when auth-daemon mode is 'remote'"
}
);
// Schema for individual resource // Schema for individual resource
export const PublicResourceSchema = z export const ResourceSchema = z
.object({ .object({
name: z.string().optional(), name: z.string().optional(),
protocol: z protocol: z.enum(["http", "tcp", "udp"]).optional(),
.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"])
.optional(), // this was the old one and is now DEPRECATED in favor of the mode
mode: z.enum(["http", "tcp", "udp", "ssh", "rdp", "vnc"]).optional(),
policy: z.string().optional(),
ssl: z.boolean().optional(), ssl: z.boolean().optional(),
scheme: z.enum(["http", "https"]).optional(), scheme: z.enum(["http", "https"]).optional(),
"full-domain": z.string().optional(), "full-domain": z.string().optional(),
@@ -200,10 +177,7 @@ export const PublicResourceSchema = z
"tls-server-name": z.string().optional(), "tls-server-name": z.string().optional(),
headers: z.array(HeaderSchema).optional(), headers: z.array(HeaderSchema).optional(),
rules: z.array(RuleSchema).optional(), rules: z.array(RuleSchema).optional(),
maintenance: MaintenanceSchema.optional(), maintenance: MaintenanceSchema.optional()
"auth-daemon": AuthDaemonSchema.optional(),
"proxy-protocol": z.boolean().optional(),
"proxy-protocol-version": z.int().min(1).optional()
}) })
.refine( .refine(
(resource) => { (resource) => {
@@ -211,10 +185,9 @@ export const PublicResourceSchema = z
return true; return true;
} }
// Otherwise, require name and protocol/mode for full resource definition // Otherwise, require name and protocol for full resource definition
return ( return (
resource.name !== undefined && resource.name !== undefined && resource.protocol !== undefined
(resource.mode !== undefined || resource.protocol !== undefined)
); );
}, },
{ {
@@ -228,8 +201,8 @@ export const PublicResourceSchema = z
return true; return true;
} }
// If protocol/mode is http, all targets must have method field // If protocol is http, all targets must have method field
if ((resource.mode ?? resource.protocol) === "http") { if (resource.protocol === "http") {
return resource.targets.every( return resource.targets.every(
(target) => target == null || target.method !== undefined (target) => target == null || target.method !== undefined
); );
@@ -247,9 +220,8 @@ export const PublicResourceSchema = z
return true; return true;
} }
// If protocol/mode is tcp or udp, no target should have method field // If protocol is tcp or udp, no target should have method field
const effectiveProtocol1 = resource.mode ?? resource.protocol; if (resource.protocol === "tcp" || resource.protocol === "udp") {
if (effectiveProtocol1 === "tcp" || effectiveProtocol1 === "udp") {
return resource.targets.every( return resource.targets.every(
(target) => target == null || target.method === undefined (target) => target == null || target.method === undefined
); );
@@ -267,8 +239,8 @@ export const PublicResourceSchema = z
return true; return true;
} }
// If protocol/mode is http, it must have a full-domain // If protocol is http, it must have a full-domain
if ((resource.mode ?? resource.protocol) === "http") { if (resource.protocol === "http") {
return ( return (
resource["full-domain"] !== undefined && resource["full-domain"] !== undefined &&
resource["full-domain"].length > 0 resource["full-domain"].length > 0
@@ -287,9 +259,8 @@ export const PublicResourceSchema = z
return true; return true;
} }
// If protocol/mode is tcp or udp, it must have both proxy-port // If protocol is tcp or udp, it must have both proxy-port
const effectiveProtocol2 = resource.mode ?? resource.protocol; if (resource.protocol === "tcp" || resource.protocol === "udp") {
if (effectiveProtocol2 === "tcp" || effectiveProtocol2 === "udp") {
return resource["proxy-port"] !== undefined; return resource["proxy-port"] !== undefined;
} }
return true; return true;
@@ -306,9 +277,8 @@ export const PublicResourceSchema = z
return true; return true;
} }
// If protocol/mode is tcp or udp, it must not have auth // If protocol is tcp or udp, it must not have auth
const effectiveProtocol3 = resource.mode ?? resource.protocol; if (resource.protocol === "tcp" || resource.protocol === "udp") {
if (effectiveProtocol3 === "tcp" || effectiveProtocol3 === "udp") {
return resource.auth === undefined; return resource.auth === undefined;
} }
return true; return true;
@@ -370,8 +340,7 @@ export const PublicResourceSchema = z
if (parts.includes("*", 1)) return false; // no further wildcards if (parts.includes("*", 1)) return false; // no further wildcards
if (parts.length < 3) return false; // need at least *.label.tld if (parts.length < 3) return false; // need at least *.label.tld
const labelRegex = const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
return parts.slice(1).every((label) => labelRegex.test(label)); return parts.slice(1).every((label) => labelRegex.test(label));
}, },
{ {
@@ -379,46 +348,22 @@ export const PublicResourceSchema = z
message: message:
'Wildcard full-domain must have "*" as the leftmost label only, followed by at least two valid hostname labels (e.g. "*.example.com" or "*.level1.example.com"). Patterns like "*example.com" or "level2.*.example.com" are not supported.' 'Wildcard full-domain must have "*" as the leftmost label only, followed by at least two valid hostname labels (e.g. "*.example.com" or "*.level1.example.com"). Patterns like "*example.com" or "level2.*.example.com" are not supported.'
} }
) );
.refine(
(resource) => {
const effectiveMode = resource.mode ?? resource.protocol;
if (effectiveMode !== "tcp") {
return (
resource["proxy-protocol"] === undefined &&
resource["proxy-protocol-version"] === undefined
);
}
return true;
},
{
path: ["proxy-protocol"],
message:
"'proxy-protocol' and 'proxy-protocol-version' can only be set when mode is 'tcp'"
}
)
.transform((resource) => {
// Normalize: prefer mode, fall back to protocol for backwards compatibility
if (resource.mode === undefined && resource.protocol !== undefined) {
resource.mode = resource.protocol;
}
return resource;
});
export function isTargetsOnlyResource(resource: any): boolean { export function isTargetsOnlyResource(resource: any): boolean {
return Object.keys(resource).length === 1 && resource.targets; return Object.keys(resource).length === 1 && resource.targets;
} }
export const PrivateResourceSchema = z export const ClientResourceSchema = z
.object({ .object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
mode: z.enum(["host", "cidr", "http", "ssh"]), mode: z.enum(["host", "cidr", "http"]),
site: z.string().optional(), // DEPRECATED IN FAVOR OF sites site: z.string().optional(), // DEPRECATED IN FAVOR OF sites
sites: z.array(z.string()).optional().default([]), sites: z.array(z.string()).optional().default([]),
// protocol: z.enum(["tcp", "udp"]).optional(), // protocol: z.enum(["tcp", "udp"]).optional(),
// proxyPort: z.int().positive().optional(), // proxyPort: z.int().positive().optional(),
"destination-port": z.int().positive().optional(), "destination-port": z.int().positive().optional(),
destination: z.string().min(1).optional(), destination: z.string().min(1),
// enabled: z.boolean().default(true), // enabled: z.boolean().default(true),
"tcp-ports": portRangeStringSchema.optional().default("*"), "tcp-ports": portRangeStringSchema.optional().default("*"),
"udp-ports": portRangeStringSchema.optional().default("*"), "udp-ports": portRangeStringSchema.optional().default("*"),
@@ -441,31 +386,11 @@ export const PrivateResourceSchema = z
error: "Admin role cannot be included in roles" error: "Admin role cannot be included in roles"
}), }),
users: z.array(z.string()).optional().default([]), users: z.array(z.string()).optional().default([]),
machines: z.array(z.string()).optional().default([]), machines: z.array(z.string()).optional().default([])
"auth-daemon": AuthDaemonSchema.optional()
}) })
.refine(
(data) => {
// destination is optional only for ssh+native; required for everything else
const isNativeSSH =
data.mode === "ssh" &&
(data["auth-daemon"] === undefined ||
data["auth-daemon"].mode === "native");
if (!isNativeSSH && !data.destination) {
return false;
}
return true;
},
{
path: ["destination"],
message:
"destination is required unless mode is 'ssh' with auth-daemon mode 'native'"
}
)
.refine( .refine(
(data) => { (data) => {
if (data.mode === "host") { if (data.mode === "host") {
if (!data.destination) return true; // caught by the destination-required refine
// Check if it's a valid IP address using zod (v4 or v6) // Check if it's a valid IP address using zod (v4 or v6)
const isValidIP = z const isValidIP = z
.union([z.ipv4(), z.ipv6()]) .union([z.ipv4(), z.ipv6()])
@@ -493,7 +418,6 @@ export const PrivateResourceSchema = z
.refine( .refine(
(data) => { (data) => {
if (data.mode === "cidr") { if (data.mode === "cidr") {
if (!data.destination) return true; // caught by the destination-required refine
// Check if it's a valid CIDR (v4 or v6) // Check if it's a valid CIDR (v4 or v6)
const isValidCIDR = z const isValidCIDR = z
.union([z.cidrv4(), z.cidrv6()]) .union([z.cidrv4(), z.cidrv6()])
@@ -511,19 +435,19 @@ export const PrivateResourceSchema = z
export const ConfigSchema = z export const ConfigSchema = z
.object({ .object({
"proxy-resources": z "proxy-resources": z
.record(z.string(), PublicResourceSchema) .record(z.string(), ResourceSchema)
.optional() .optional()
.prefault({}), .prefault({}),
"public-resources": z "public-resources": z
.record(z.string(), PublicResourceSchema) .record(z.string(), ResourceSchema)
.optional() .optional()
.prefault({}), .prefault({}),
"client-resources": z "client-resources": z
.record(z.string(), PrivateResourceSchema) .record(z.string(), ClientResourceSchema)
.optional() .optional()
.prefault({}), .prefault({}),
"private-resources": z "private-resources": z
.record(z.string(), PrivateResourceSchema) .record(z.string(), ClientResourceSchema)
.optional() .optional()
.prefault({}), .prefault({}),
sites: z.record(z.string(), SiteSchema).optional().prefault({}) sites: z.record(z.string(), SiteSchema).optional().prefault({})
@@ -548,13 +472,10 @@ export const ConfigSchema = z
} }
return data as { return data as {
"proxy-resources": Record< "proxy-resources": Record<string, z.infer<typeof ResourceSchema>>;
string,
z.infer<typeof PublicResourceSchema>
>;
"client-resources": Record< "client-resources": Record<
string, string,
z.infer<typeof PrivateResourceSchema> z.infer<typeof ClientResourceSchema>
>; >;
sites: Record<string, z.infer<typeof SiteSchema>>; sites: Record<string, z.infer<typeof SiteSchema>>;
}; };
@@ -693,5 +614,5 @@ export const ConfigSchema = z
// Type inference from the schema // Type inference from the schema
export type Site = z.infer<typeof SiteSchema>; export type Site = z.infer<typeof SiteSchema>;
export type Target = z.infer<typeof TargetSchema>; export type Target = z.infer<typeof TargetSchema>;
export type Resource = z.infer<typeof PublicResourceSchema>; export type Resource = z.infer<typeof ResourceSchema>;
export type Config = z.infer<typeof ConfigSchema>; export type Config = z.infer<typeof ConfigSchema>;
+10 -3
View File
@@ -331,8 +331,16 @@ export async function calculateUserClientsForOrgs(
]; ];
// Get next available subnet // Get next available subnet
const { value: newSubnet, release: releaseSubnetLock } = const newSubnet = await getNextAvailableClientSubnet(
await getNextAvailableClientSubnet(orgId, transaction); orgId,
transaction
);
if (!newSubnet) {
logger.warn(
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no available subnet found`
);
continue;
}
const subnet = newSubnet.split("/")[0]; const subnet = newSubnet.split("/")[0];
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
@@ -362,7 +370,6 @@ export async function calculateUserClientsForOrgs(
.insert(clients) .insert(clients)
.values(newClientData) .values(newClientData)
.returning(); .returning();
await releaseSubnetLock();
existingClientCache.set( existingClientCache.set(
getOrgOlmKey(orgId, olm.olmId), getOrgOlmKey(orgId, olm.olmId),
newClient newClient
+1 -1
View File
@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process // This is a placeholder value replaced by the build process
export const APP_VERSION = "1.19.0"; export const APP_VERSION = "1.18.4";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);
+115 -144
View File
@@ -327,145 +327,127 @@ export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
export async function getNextAvailableClientSubnet( export async function getNextAvailableClientSubnet(
orgId: string, orgId: string,
transaction: Transaction | typeof db = db transaction: Transaction | typeof db = db
): Promise<{ value: string; release: () => Promise<void> }> { ): Promise<string> {
const lockKey = `client-subnet-allocation:${orgId}`; return await lockManager.withLock(
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000); `client-subnet-allocation:${orgId}`,
if (!acquired) { async () => {
throw new Error(`Failed to acquire lock: ${lockKey}`); const [org] = await transaction
} .select()
const release = () => lockManager.releaseLock(lockKey); .from(orgs)
.where(eq(orgs.orgId, orgId));
try { if (!org) {
const [org] = await transaction throw new Error(`Organization with ID ${orgId} not found`);
.select() }
.from(orgs)
.where(eq(orgs.orgId, orgId));
if (!org) { if (!org.subnet) {
throw new Error(`Organization with ID ${orgId} not found`); throw new Error(
`Organization with ID ${orgId} has no subnet defined`
);
}
const existingAddressesSites = await transaction
.select({
address: sites.address
})
.from(sites)
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
const existingAddressesClients = await transaction
.select({
address: clients.subnet
})
.from(clients)
.where(
and(isNotNull(clients.subnet), eq(clients.orgId, orgId))
);
const addresses = [
...existingAddressesSites.map(
(site) => `${site.address?.split("/")[0]}/32`
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
...existingAddressesClients.map(
(client) => `${client.address.split("/")}/32`
)
].filter((address) => address !== null) as string[];
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
return subnet;
} }
);
if (!org.subnet) {
throw new Error(
`Organization with ID ${orgId} has no subnet defined`
);
}
const existingAddressesSites = await transaction
.select({
address: sites.address
})
.from(sites)
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
const existingAddressesClients = await transaction
.select({
address: clients.subnet
})
.from(clients)
.where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));
const addresses = [
...existingAddressesSites.map(
(site) => `${site.address?.split("/")[0]}/32`
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
...existingAddressesClients.map(
(client) => `${client.address.split("/")[0]}/32`
)
].filter((address) => address !== null) as string[];
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
return { value: subnet, release };
} catch (e) {
await release();
throw e;
}
} }
export async function getNextAvailableAliasAddress( export async function getNextAvailableAliasAddress(
orgId: string, orgId: string,
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<{ value: string; release: () => Promise<void> }> { ): Promise<string> {
const lockKey = `alias-address-allocation:${orgId}`; return await lockManager.withLock(
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000); `alias-address-allocation:${orgId}`,
if (!acquired) { async () => {
throw new Error(`Failed to acquire lock: ${lockKey}`); const [org] = await trx
} .select()
const release = () => lockManager.releaseLock(lockKey); .from(orgs)
.where(eq(orgs.orgId, orgId));
try { if (!org) {
const [org] = await trx throw new Error(`Organization with ID ${orgId} not found`);
.select() }
.from(orgs)
.where(eq(orgs.orgId, orgId));
if (!org) { if (!org.subnet) {
throw new Error(`Organization with ID ${orgId} not found`); throw new Error(
} `Organization with ID ${orgId} has no subnet defined`
);
}
if (!org.subnet) { if (!org.utilitySubnet) {
throw new Error( throw new Error(
`Organization with ID ${orgId} has no subnet defined` `Organization with ID ${orgId} has no utility subnet defined`
);
}
const existingAddresses = await trx
.select({
aliasAddress: siteResources.aliasAddress
})
.from(siteResources)
.where(
and(
isNotNull(siteResources.aliasAddress),
eq(siteResources.orgId, orgId)
)
);
const addresses = [
...existingAddresses.map(
(site) => `${site.aliasAddress?.split("/")[0]}/32`
),
// reserve a /29 for the dns server and other stuff
`${org.utilitySubnet.split("/")[0]}/29`
].filter((address) => address !== null) as string[];
let subnet = findNextAvailableCidr(
addresses,
32,
org.utilitySubnet
); );
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
// remove the cidr
subnet = subnet.split("/")[0];
return subnet;
} }
);
if (!org.utilitySubnet) {
throw new Error(
`Organization with ID ${orgId} has no utility subnet defined`
);
}
const existingAddresses = await trx
.select({
aliasAddress: siteResources.aliasAddress
})
.from(siteResources)
.where(
and(
isNotNull(siteResources.aliasAddress),
eq(siteResources.orgId, orgId)
)
);
const addresses = [
...existingAddresses.map(
(site) => `${site.aliasAddress?.split("/")[0]}/32`
),
// reserve a /29 for the dns server and other stuff
`${org.utilitySubnet.split("/")[0]}/29`
].filter((address) => address !== null) as string[];
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
// remove the cidr
subnet = subnet.split("/")[0];
return { value: subnet, release };
} catch (e) {
await release();
throw e;
}
} }
export async function getNextAvailableOrgSubnet(): Promise<{ export async function getNextAvailableOrgSubnet(): Promise<string> {
value: string; return await lockManager.withLock("org-subnet-allocation", async () => {
release: () => Promise<void>;
}> {
const lockKey = "org-subnet-allocation";
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
if (!acquired) {
throw new Error(`Failed to acquire lock: ${lockKey}`);
}
const release = () => lockManager.releaseLock(lockKey);
try {
const existingAddresses = await db const existingAddresses = await db
.select({ .select({
subnet: orgs.subnet subnet: orgs.subnet
@@ -484,11 +466,8 @@ export async function getNextAvailableOrgSubnet(): Promise<{
throw new Error("No available subnets remaining in space"); throw new Error("No available subnets remaining in space");
} }
return { value: subnet, release }; return subnet;
} catch (e) { });
await release();
throw e;
}
} }
export function generateRemoteSubnets( export function generateRemoteSubnets(
@@ -496,8 +475,6 @@ export function generateRemoteSubnets(
): string[] { ): string[] {
const remoteSubnets = allSiteResources const remoteSubnets = allSiteResources
.filter((sr) => { .filter((sr) => {
if (!sr.destination) return false;
if (sr.mode === "cidr") { if (sr.mode === "cidr") {
// check if its a valid CIDR using zod // check if its a valid CIDR using zod
const cidrSchema = z.union([z.cidrv4(), z.cidrv6()]); const cidrSchema = z.union([z.cidrv4(), z.cidrv6()]);
@@ -519,7 +496,7 @@ export function generateRemoteSubnets(
} }
return ""; // This should never be reached due to filtering, but satisfies TypeScript return ""; // This should never be reached due to filtering, but satisfies TypeScript
}) })
.filter((subnet): subnet is string => subnet !== "" && subnet !== null); // Remove invalid values just to be safe .filter((subnet) => subnet !== ""); // Remove empty strings just to be safe
// remove duplicates // remove duplicates
return Array.from(new Set(remoteSubnets)); return Array.from(new Set(remoteSubnets));
} }
@@ -604,7 +581,7 @@ export function generateSubnetProxyTargets(
targets.push({ targets.push({
sourcePrefix: clientPrefix, sourcePrefix: clientPrefix,
destPrefix: `${siteResource.aliasAddress}/32`, destPrefix: `${siteResource.aliasAddress}/32`,
rewriteTo: destination!, rewriteTo: destination,
portRange, portRange,
disableIcmp disableIcmp
}); });
@@ -612,7 +589,7 @@ export function generateSubnetProxyTargets(
} else if (siteResource.mode == "cidr") { } else if (siteResource.mode == "cidr") {
targets.push({ targets.push({
sourcePrefix: clientPrefix, sourcePrefix: clientPrefix,
destPrefix: siteResource.destination!, destPrefix: siteResource.destination,
portRange, portRange,
disableIcmp disableIcmp
}); });
@@ -694,7 +671,7 @@ export async function generateSubnetProxyTargetV2(
targets.push({ targets.push({
sourcePrefixes: [], sourcePrefixes: [],
destPrefix: `${siteResource.aliasAddress}/32`, destPrefix: `${siteResource.aliasAddress}/32`,
rewriteTo: destination!, rewriteTo: destination,
portRange, portRange,
disableIcmp, disableIcmp,
resourceId: siteResource.siteResourceId resourceId: siteResource.siteResourceId
@@ -703,7 +680,7 @@ export async function generateSubnetProxyTargetV2(
} else if (siteResource.mode == "cidr") { } else if (siteResource.mode == "cidr") {
targets.push({ targets.push({
sourcePrefixes: [], sourcePrefixes: [],
destPrefix: siteResource.destination!, destPrefix: siteResource.destination,
portRange, portRange,
disableIcmp, disableIcmp,
resourceId: siteResource.siteResourceId resourceId: siteResource.siteResourceId
@@ -761,7 +738,7 @@ export async function generateSubnetProxyTargetV2(
protocol: siteResource.ssl ? "https" : "http", protocol: siteResource.ssl ? "https" : "http",
httpTargets: [ httpTargets: [
{ {
destAddr: siteResource.destination!, destAddr: siteResource.destination,
destPort: siteResource.destinationPort, destPort: siteResource.destinationPort,
scheme: siteResource.scheme scheme: siteResource.scheme
} }
@@ -896,13 +873,7 @@ export const portRangeStringSchema = z
message: message:
'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535, and ranges must have start <= end.' 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535, and ranges must have start <= end.'
} }
) );
.openapi({
type: "string",
description:
'Port range string. Use "*" for all ports, a comma-separated list of ports, or ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.',
example: "80,443,8000-9000"
});
/** /**
* Parses a port range string into an array of port range objects * Parses a port range string into an array of port range objects
@@ -1,11 +0,0 @@
import { z } from "zod";
export function createApiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
return z.object({
data: dataSchema.nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
});
}
-80
View File
@@ -890,9 +890,6 @@ async function handleSubnetProxyTargetUpdates(
} }
for (const client of removedClients) { for (const client of removedClients) {
if (!siteResource.destination) {
continue;
}
// Check if this client still has access to another resource // Check if this client still has access to another resource
// on this specific site with the same destination. We scope // on this specific site with the same destination. We scope
// by siteId (via siteNetworks) rather than networkId because // by siteId (via siteNetworks) rather than networkId because
@@ -1566,9 +1563,6 @@ async function handleMessagesForClientResources(
} }
try { try {
if (!resource.destination) {
continue;
}
// Check if this client still has access to another resource // Check if this client still has access to another resource
// on this specific site with the same destination. We scope // on this specific site with the same destination. We scope
// by siteId (via siteNetworks) rather than networkId because // by siteId (via siteNetworks) rather than networkId because
@@ -1832,77 +1826,3 @@ export async function verifyClientAssociationsCache(
extraSiteIds: extraSiteIds.sort((a, b) => a - b) extraSiteIds: extraSiteIds.sort((a, b) => a - b)
}; };
} }
// cleanupSiteAssociations efficiently removes all client associations for a
// site that is being deleted. Instead of calling
// rebuildClientAssociationsFromSiteResource once per site resource (which is
// O(resources) in DB round-trips and message fan-out), this function performs
// a single bulk lookup of affected clients and site resources, deletes all
// cache rows at once, and fires all peer/proxy removal messages in parallel.
//
// The caller is responsible for deleting the site row itself (and for sending
// the newt/wg/terminate signal to the newt process).
export async function cleanupSiteAssociations(
site: Site,
trx: Transaction | typeof db = db
): Promise<void> {
const siteId = site.siteId;
logger.debug(`cleanupSiteAssociations: START siteId=${siteId}`);
// 1. Find every client currently cached against this site.
const cachedSiteClientRows = await trx
.select({ clientId: clientSitesAssociationsCache.clientId })
.from(clientSitesAssociationsCache)
.where(eq(clientSitesAssociationsCache.siteId, siteId));
const cachedClientIds = cachedSiteClientRows.map((r) => r.clientId);
// 2. Load full client details (needed for WireGuard public-key references).
const allClients =
cachedClientIds.length > 0
? await trx
.select({
clientId: clients.clientId,
pubKey: clients.pubKey,
subnet: clients.subnet
})
.from(clients)
.where(inArray(clients.clientId, cachedClientIds))
: [];
// 6. Bulk-delete all cache entries for this site. Do this before sending
// destination-update messages so updateClientSiteDestinations computes
// the correct (post-deletion) set of destinations.
await trx
.delete(clientSitesAssociationsCache)
.where(eq(clientSitesAssociationsCache.siteId, siteId));
logger.debug(
`cleanupSiteAssociations: siteId=${siteId} cache cleared. clients=${allClients.length}`
);
// 7. Fire all removal messages in parallel.
const jobs: Promise<any>[] = [];
for (const client of allClients) {
// Tell each olm to drop the site's WireGuard peer.
if (site.publicKey) {
jobs.push(olmDeletePeer(client.clientId, siteId, site.publicKey));
}
// Recompute and push updated relay destinations (now excluding this site).
if (client.pubKey && client.subnet) {
jobs.push(updateClientSiteDestinations(client, trx));
}
}
await Promise.all(jobs).catch((error) => {
logger.error(
`cleanupSiteAssociations: error sending cleanup messages for siteId=${siteId}:`,
error
);
});
logger.debug(`cleanupSiteAssociations: DONE siteId=${siteId}`);
}
+4 -10
View File
@@ -2,14 +2,7 @@ import { PostHog } from "posthog-node";
import config from "./config"; import config from "./config";
import { getHostMeta } from "./hostMeta"; import { getHostMeta } from "./hostMeta";
import logger from "@server/logger"; import logger from "@server/logger";
import { import { alertRules, apiKeys, blueprints, db, roles, siteResources } from "@server/db";
alertRules,
apiKeys,
blueprints,
db,
roles,
siteResources
} from "@server/db";
import { sites, users, orgs, resources, clients, idp } from "@server/db"; import { sites, users, orgs, resources, clients, idp } from "@server/db";
import { eq, count, notInArray, and, isNotNull, isNull } from "drizzle-orm"; import { eq, count, notInArray, and, isNotNull, isNull } from "drizzle-orm";
import { APP_VERSION } from "./consts"; import { APP_VERSION } from "./consts";
@@ -150,7 +143,8 @@ class TelemetryClient {
.select({ .select({
name: resources.name, name: resources.name,
sso: resources.sso, sso: resources.sso,
mode: resources.mode protocol: resources.protocol,
http: resources.http
}) })
.from(resources); .from(resources);
@@ -317,7 +311,7 @@ class TelemetryClient {
(r) => r.sso (r) => r.sso
).length, ).length,
num_resources_non_http: stats.resources.filter( num_resources_non_http: stats.resources.filter(
(r) => r.mode !== "http" (r) => !r.http
).length, ).length,
num_newt_sites: stats.sites.filter((s) => s.type === "newt") num_newt_sites: stats.sites.filter((s) => s.type === "newt")
.length, .length,
+1 -2
View File
@@ -520,8 +520,7 @@ export class TraefikConfigManager {
build != "oss", // generate the login pages on the cloud and hybrid, build != "oss", // generate the login pages on the cloud and hybrid,
build == "saas" build == "saas"
? false ? false
: config.getRawConfig().traefik.allow_raw_resources, // dont allow raw resources on saas otherwise use config : config.getRawConfig().traefik.allow_raw_resources // dont allow raw resources on saas otherwise use config
build != "oss" // generate browser gateway targets on cloud and enterprise
); );
const domains = new Set<string>(); const domains = new Set<string>();
+7 -5
View File
@@ -55,7 +55,9 @@ export async function getTraefikConfig(
resourceName: resources.name, resourceName: resources.name,
fullDomain: resources.fullDomain, fullDomain: resources.fullDomain,
ssl: resources.ssl, ssl: resources.ssl,
http: resources.http,
proxyPort: resources.proxyPort, proxyPort: resources.proxyPort,
protocol: resources.protocol,
subdomain: resources.subdomain, subdomain: resources.subdomain,
domainId: resources.domainId, domainId: resources.domainId,
enabled: resources.enabled, enabled: resources.enabled,
@@ -66,7 +68,6 @@ export async function getTraefikConfig(
headers: resources.headers, headers: resources.headers,
proxyProtocol: resources.proxyProtocol, proxyProtocol: resources.proxyProtocol,
proxyProtocolVersion: resources.proxyProtocolVersion, proxyProtocolVersion: resources.proxyProtocolVersion,
mode: resources.mode,
// Target fields // Target fields
targetId: targets.targetId, targetId: targets.targetId,
@@ -114,8 +115,8 @@ export async function getTraefikConfig(
), ),
inArray(sites.type, siteTypes), inArray(sites.type, siteTypes),
allowRawResources allowRawResources
? inArray(resources.mode, ["http", "udp", "tcp"]) // allow all three ? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
: eq(resources.mode, "http") : eq(resources.http, true)
) )
) )
.orderBy(desc(targets.priority), targets.targetId); // stable ordering .orderBy(desc(targets.priority), targets.targetId); // stable ordering
@@ -165,8 +166,9 @@ export async function getTraefikConfig(
key: key, key: key,
fullDomain: row.fullDomain, fullDomain: row.fullDomain,
ssl: row.ssl, ssl: row.ssl,
mode: row.mode, http: row.http,
proxyPort: row.proxyPort, proxyPort: row.proxyPort,
protocol: row.protocol,
subdomain: row.subdomain, subdomain: row.subdomain,
domainId: row.domainId, domainId: row.domainId,
enabled: row.enabled, enabled: row.enabled,
@@ -578,7 +580,7 @@ export async function getTraefikConfig(
continue; continue;
} }
const protocol = resource.mode === "udp" ? "udp" : "tcp"; // all of the other ones are tcp const protocol = resource.protocol.toLowerCase();
const port = resource.proxyPort; const port = resource.proxyPort;
if (!port) { if (!port) {
-1
View File
@@ -244,5 +244,4 @@ try {
runTests(); runTests();
} catch (error) { } catch (error) {
console.error("Test failed:", error); console.error("Test failed:", error);
process.exit(1);
} }
-2
View File
@@ -28,9 +28,7 @@ export * from "./verifyApiKeyAccess";
export * from "./verifySiteProvisioningKeyAccess"; export * from "./verifySiteProvisioningKeyAccess";
export * from "./verifyDomainAccess"; export * from "./verifyDomainAccess";
export * from "./verifyUserIsOrgOwner"; export * from "./verifyUserIsOrgOwner";
export * from "./verifyUserFromResourceSession";
export * from "./verifySiteResourceAccess"; export * from "./verifySiteResourceAccess";
export * from "./logActionAudit"; export * from "./logActionAudit";
export * from "./verifyOlmAccess"; export * from "./verifyOlmAccess";
export * from "./verifyLimits"; export * from "./verifyLimits";
export * from "./verifyResourcePolicyAccess";
-1
View File
@@ -16,4 +16,3 @@ export * from "./verifyApiKeyClientAccess";
export * from "./verifyApiKeySiteResourceAccess"; export * from "./verifyApiKeySiteResourceAccess";
export * from "./verifyApiKeyIdpAccess"; export * from "./verifyApiKeyIdpAccess";
export * from "./verifyApiKeyDomainAccess"; export * from "./verifyApiKeyDomainAccess";
export * from "./verifyApiKeyResourcePolicyAccess";
@@ -1,92 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { resourcePolicies, apiKeyOrg } from "@server/db";
import { eq, and } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeyResourcePolicyAccess(
req: Request,
res: Response,
next: NextFunction
) {
const apiKey = req.apiKey;
const resourcePolicyId =
req.params.resourcePolicyId ||
req.body.resourcePolicyId ||
req.query.resourcePolicyId;
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
try {
// Retrieve the resource policy
const [policy] = await db
.select()
.from(resourcePolicies)
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
.limit(1);
if (!policy) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource policy with ID ${resourcePolicyId} not found`
)
);
}
if (apiKey.isRoot) {
// Root keys can access any resource policy in any org
return next();
}
if (!policy.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Resource policy with ID ${resourcePolicyId} does not have an organization ID`
)
);
}
// Verify that the API key is linked to the resource policy's organization
if (!req.apiKeyOrg) {
const apiKeyOrgResult = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, policy.orgId)
)
)
.limit(1);
if (apiKeyOrgResult.length > 0) {
req.apiKeyOrg = apiKeyOrgResult[0];
}
}
if (!req.apiKeyOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying resource policy access"
)
);
}
}
@@ -1,127 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { resourcePolicies, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyResourcePolicyAccess(
req: Request,
res: Response,
next: NextFunction
) {
const userId = req.user!.userId;
const resourcePolicyIdStr =
req.params?.resourcePolicyId ||
req.body?.resourcePolicyId ||
req.query?.resourcePolicyId;
const niceId = req.params?.niceId || req.body?.niceId || req.query?.niceId;
const orgId = req.params?.orgId || req.body?.orgId || req.query?.orgId;
try {
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
let policy: typeof resourcePolicies.$inferSelect | null = null;
if (orgId && niceId) {
const [policyRes] = await db
.select()
.from(resourcePolicies)
.where(
and(
eq(resourcePolicies.niceId, niceId),
eq(resourcePolicies.orgId, orgId)
)
)
.limit(1);
policy = policyRes ?? null;
} else {
const resourcePolicyId = parseInt(resourcePolicyIdStr);
if (isNaN(resourcePolicyId)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid resource policy ID"
)
);
}
const [policyRes] = await db
.select()
.from(resourcePolicies)
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId))
.limit(1);
policy = policyRes ?? null;
}
if (!policy) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource policy with ID ${resourcePolicyIdStr ?? niceId} not found`
)
);
}
if (!req.userOrg) {
const userOrgRes = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, policy.orgId)
)
)
.limit(1);
req.userOrg = userOrgRes[0];
}
if (!req.userOrg || req.userOrg.orgId !== policy.orgId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
policy.orgId
);
req.userOrgId = policy.orgId;
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying resource policy access"
)
);
}
}
@@ -38,7 +38,7 @@ export function verifyUserCanSetUserOrgRoles() {
return next( return next(
createHttpError( createHttpError(
HttpCode.FORBIDDEN, HttpCode.FORBIDDEN,
"User does not have permission to set user organization roles" "User does not have permission perform this action"
) )
); );
} catch (error) { } catch (error) {
@@ -1,102 +0,0 @@
import { Response, NextFunction } from "express";
import { db } from "@server/db";
import { resourceSessions, users } from "@server/db";
import { eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
import { getUserSessionWithUser } from "@server/db/queries/verifySessionQueries";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import config from "@server/lib/config";
import logger from "@server/logger";
export const verifyUserFromResourceSessionMiddleware = async (
req: any,
res: Response,
next: NextFunction
) => {
if (!req.user) {
const sessionCookieName =
config.getRawConfig().server.session_cookie_name;
// Collect all resource session cookies (format: {name}[_s].{timestamp}=token)
const cookieHeader: string | undefined = req.headers.cookie;
const candidates: { timestamp: number; token: string }[] = [];
if (cookieHeader) {
for (const part of cookieHeader.split(";")) {
const trimmed = part.trim();
const eqIdx = trimmed.indexOf("=");
if (eqIdx === -1) continue;
const cookieName = trimmed.slice(0, eqIdx).trim();
const cookieValue = trimmed.slice(eqIdx + 1).trim();
// Match both secure (_s.timestamp) and non-secure (.timestamp) variants
const securePrefix = `${sessionCookieName}_s.`;
const httpPrefix = `${sessionCookieName}.`;
let timestampStr: string | null = null;
if (cookieName.startsWith(securePrefix)) {
timestampStr = cookieName.slice(securePrefix.length);
} else if (cookieName.startsWith(httpPrefix)) {
timestampStr = cookieName.slice(httpPrefix.length);
}
if (timestampStr !== null && /^\d+$/.test(timestampStr)) {
candidates.push({
timestamp: parseInt(timestampStr, 10),
token: cookieValue
});
}
}
}
// Pick the most recently issued session (highest timestamp)
candidates.sort((a, b) => b.timestamp - a.timestamp);
const best = candidates[0];
if (best) {
try {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(best.token))
);
const [resourceSession] = await db
.select()
.from(resourceSessions)
.where(eq(resourceSessions.sessionId, sessionId))
.limit(1);
if (resourceSession && Date.now() < resourceSession.expiresAt) {
if (resourceSession.userSessionId) {
const result = await getUserSessionWithUser(
resourceSession.userSessionId
);
if (result?.user && result?.session) {
req.user = result.user;
req.session = result.session;
}
}
}
} catch (e) {
logger.error(
"verifyUserFromResourceSessionMiddleware: failed to validate resource session",
e
);
}
}
}
// Populate userOrgRoleIds if an orgId is available in route params
if (req.user && req.params?.orgId && !req.userOrgRoleIds) {
req.userOrgRoleIds = await getUserOrgRoleIds(
req.user.userId,
req.params.orgId
);
}
next();
};
-1
View File
@@ -7,7 +7,6 @@ export enum OpenAPITags {
Org = "Organization", Org = "Organization",
PublicResource = "Public Resource", PublicResource = "Public Resource",
PrivateResource = "Private Resource", PrivateResource = "Private Resource",
Policy = "Policy",
Role = "Role", Role = "Role",
User = "User", User = "User",
Invitation = "User Invitation", Invitation = "User Invitation",
+3 -3
View File
@@ -780,9 +780,9 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
} }
} }
// logger.debug( logger.debug(
// `acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}` `acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}`
// ); );
for (const domain of allDomains) { for (const domain of allDomains) {
try { try {
+6 -40
View File
@@ -36,6 +36,8 @@ export async function getValidCertificatesForDomains(
domains: Set<string>, domains: Set<string>,
useCache: boolean = true useCache: boolean = true
): Promise<Array<CertificateResult>> { ): Promise<Array<CertificateResult>> {
const finalResults: CertificateResult[] = []; const finalResults: CertificateResult[] = [];
const domainsToQuery = new Set<string>(); const domainsToQuery = new Set<string>();
@@ -47,26 +49,7 @@ export async function getValidCertificatesForDomains(
if (cachedCert) { if (cachedCert) {
finalResults.push(cachedCert); // Valid cache hit finalResults.push(cachedCert); // Valid cache hit
} else { } else {
// Also check for a wildcard cache entry covering this domain's parent domainsToQuery.add(domain); // Cache miss or expired
const parts = domain.split(".");
let wildcardHit = false;
if (parts.length > 1) {
const parentDomain = parts.slice(1).join(".");
const wildcardCacheKey = `cert:*.${parentDomain}`;
const cachedWildcard =
await cache.get<CertificateResult>(wildcardCacheKey);
if (cachedWildcard) {
// Re-stamp queriedDomain so callers see the originally requested domain
finalResults.push({
...cachedWildcard,
queriedDomain: domain
});
wildcardHit = true;
}
}
if (!wildcardHit) {
domainsToQuery.add(domain); // Cache miss or expired
}
} }
} }
} else { } else {
@@ -76,10 +59,7 @@ export async function getValidCertificatesForDomains(
// 2. If all domains were resolved from the cache, return early // 2. If all domains were resolved from the cache, return early
if (domainsToQuery.size === 0) { if (domainsToQuery.size === 0) {
const decryptedResults = decryptFinalResults( const decryptedResults = decryptFinalResults(finalResults, config.getRawConfig().server.secret!);
finalResults,
config.getRawConfig().server.secret!
);
return decryptedResults; return decryptedResults;
} }
@@ -98,8 +78,7 @@ export async function getValidCertificatesForDomains(
const parentDomainsArray = Array.from(parentDomainsToQuery); const parentDomainsArray = Array.from(parentDomainsToQuery);
// Build wildcard variants: for each parent domain "example.com", also query "*.example.com" // Build wildcard variants: for each parent domain "example.com", also query "*.example.com"
const wildcardPrefixedArray = const wildcardPrefixedArray = build != "saas" ? parentDomainsArray.map((d) => `*.${d}`) : [];
build != "saas" ? parentDomainsArray.map((d) => `*.${d}`) : [];
// 4. Build and execute a single, efficient Drizzle query // 4. Build and execute a single, efficient Drizzle query
// This query fetches all potential exact and wildcard matches in one database round-trip. // This query fetches all potential exact and wildcard matches in one database round-trip.
@@ -193,24 +172,11 @@ export async function getValidCertificatesForDomains(
if (useCache) { if (useCache) {
const cacheKey = `cert:${domain}`; const cacheKey = `cert:${domain}`;
await cache.set(cacheKey, resultCert, 180); await cache.set(cacheKey, resultCert, 180);
// Also cache wildcard certs under a pattern key so other subdomains
// can find them without a DB round-trip
if (resultCert.wildcard) {
const normalizedCertDomain = normalizeWildcardDomain(
resultCert.domain
);
const wildcardCacheKey = `cert:*.${normalizedCertDomain}`;
await cache.set(wildcardCacheKey, resultCert, 180);
}
} }
} }
} }
const decryptedResults = decryptFinalResults( const decryptedResults = decryptFinalResults(finalResults, config.getRawConfig().server.secret!);
finalResults,
config.getRawConfig().server.secret!
);
return decryptedResults; return decryptedResults;
} }
+12 -407
View File
@@ -12,7 +12,6 @@
*/ */
import { import {
browserGatewayTarget,
certificates, certificates,
db, db,
domainNamespaces, domainNamespaces,
@@ -85,8 +84,7 @@ export async function getTraefikConfig(
filterOutNamespaceDomains = false, filterOutNamespaceDomains = false,
generateLoginPageRouters = false, generateLoginPageRouters = false,
allowRawResources = true, allowRawResources = true,
allowMaintenancePage = true, allowMaintenancePage = true
allowBrowserGatewayResources = true
): Promise<any> { ): Promise<any> {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
@@ -97,7 +95,9 @@ export async function getTraefikConfig(
resourceName: resources.name, resourceName: resources.name,
fullDomain: resources.fullDomain, fullDomain: resources.fullDomain,
ssl: resources.ssl, ssl: resources.ssl,
http: resources.http,
proxyPort: resources.proxyPort, proxyPort: resources.proxyPort,
protocol: resources.protocol,
subdomain: resources.subdomain, subdomain: resources.subdomain,
domainId: resources.domainId, domainId: resources.domainId,
enabled: resources.enabled, enabled: resources.enabled,
@@ -109,7 +109,6 @@ export async function getTraefikConfig(
proxyProtocol: resources.proxyProtocol, proxyProtocol: resources.proxyProtocol,
proxyProtocolVersion: resources.proxyProtocolVersion, proxyProtocolVersion: resources.proxyProtocolVersion,
wildcard: resources.wildcard, wildcard: resources.wildcard,
mode: resources.mode,
maintenanceModeEnabled: resources.maintenanceModeEnabled, maintenanceModeEnabled: resources.maintenanceModeEnabled,
maintenanceModeType: resources.maintenanceModeType, maintenanceModeType: resources.maintenanceModeType,
@@ -172,8 +171,8 @@ export async function getTraefikConfig(
), ),
inArray(sites.type, siteTypes), inArray(sites.type, siteTypes),
allowRawResources allowRawResources
? inArray(resources.mode, ["http", "udp", "tcp"]) // allow all three ? isNotNull(resources.http) // ignore the http check if allow_raw_resources is true
: eq(resources.mode, "http") : eq(resources.http, true)
) )
) )
.orderBy(desc(targets.priority), targets.targetId); // stable ordering .orderBy(desc(targets.priority), targets.targetId); // stable ordering
@@ -227,8 +226,9 @@ export async function getTraefikConfig(
key: key, key: key,
fullDomain: row.fullDomain, fullDomain: row.fullDomain,
ssl: row.ssl, ssl: row.ssl,
http: row.http,
proxyPort: row.proxyPort, proxyPort: row.proxyPort,
mode: row.mode, protocol: row.protocol,
subdomain: row.subdomain, subdomain: row.subdomain,
domainId: row.domainId, domainId: row.domainId,
enabled: row.enabled, enabled: row.enabled,
@@ -277,138 +277,10 @@ export async function getTraefikConfig(
}); });
}); });
// Group browser gateway targets by resource
type BrowserGatewayResourceEntry = {
resourceId: number;
name: string;
fullDomain: string | null;
ssl: boolean | null;
subdomain: string | null;
domainId: string | null;
enabled: boolean | null;
wildcard: boolean | null;
domainCertResolver: string | null;
preferWildcardCert: boolean | null;
maintenanceModeEnabled: boolean | null;
maintenanceModeType: string | null;
maintenanceTitle: string | null;
maintenanceMessage: string | null;
maintenanceEstimatedTime: string | null;
targets: {
browserGatewayTargetId: number;
bgType: string;
siteId: number;
siteType: string;
siteOnline: boolean | null;
subnet: string | null;
siteExitNodeId: number | null;
}[];
};
const browserGatewayResourcesMap = new Map<
number,
BrowserGatewayResourceEntry
>();
if (allowBrowserGatewayResources) {
// Query browser gateway targets for this exit node
const browserGatewayRows = await db
.select({
// Resource fields
resourceId: resources.resourceId,
resourceName: resources.name,
fullDomain: resources.fullDomain,
ssl: resources.ssl,
subdomain: resources.subdomain,
domainId: resources.domainId,
enabled: resources.enabled,
wildcard: resources.wildcard,
domainCertResolver: domains.certResolver,
preferWildcardCert: domains.preferWildcardCert,
domainNamespaceId: domainNamespaces.domainNamespaceId,
// Maintenance fields
maintenanceModeEnabled: resources.maintenanceModeEnabled,
maintenanceModeType: resources.maintenanceModeType,
maintenanceTitle: resources.maintenanceTitle,
maintenanceMessage: resources.maintenanceMessage,
maintenanceEstimatedTime: resources.maintenanceEstimatedTime,
// Browser gateway target fields
browserGatewayTargetId:
browserGatewayTarget.browserGatewayTargetId,
bgType: browserGatewayTarget.type,
// Site fields
siteId: sites.siteId,
siteType: sites.type,
siteOnline: sites.online,
subnet: sites.subnet,
siteExitNodeId: sites.exitNodeId
})
.from(browserGatewayTarget)
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
.innerJoin(
resources,
eq(resources.resourceId, browserGatewayTarget.resourceId)
)
.leftJoin(domains, eq(domains.domainId, resources.domainId))
.leftJoin(
domainNamespaces,
eq(domainNamespaces.domainId, resources.domainId)
)
.where(
and(
eq(resources.enabled, true),
or(
eq(sites.exitNodeId, exitNodeId),
and(
isNull(sites.exitNodeId),
sql`(${siteTypes.includes("local") ? 1 : 0} = 1)`,
eq(sites.type, "local"),
sql`(${build != "saas" ? 1 : 0} = 1)`
)
),
inArray(sites.type, siteTypes)
)
);
for (const row of browserGatewayRows) {
if (filterOutNamespaceDomains && row.domainNamespaceId) {
continue;
}
if (!browserGatewayResourcesMap.has(row.resourceId)) {
browserGatewayResourcesMap.set(row.resourceId, {
resourceId: row.resourceId,
name: sanitize(row.resourceName) || "",
fullDomain: row.fullDomain,
ssl: row.ssl,
subdomain: row.subdomain,
domainId: row.domainId,
enabled: row.enabled,
wildcard: row.wildcard,
domainCertResolver: row.domainCertResolver,
preferWildcardCert: row.preferWildcardCert,
maintenanceModeEnabled: row.maintenanceModeEnabled,
maintenanceModeType: row.maintenanceModeType,
maintenanceTitle: row.maintenanceTitle,
maintenanceMessage: row.maintenanceMessage,
maintenanceEstimatedTime: row.maintenanceEstimatedTime,
targets: []
});
}
browserGatewayResourcesMap.get(row.resourceId)!.targets.push({
browserGatewayTargetId: row.browserGatewayTargetId,
bgType: row.bgType,
siteId: row.siteId,
siteType: row.siteType,
siteOnline: row.siteOnline,
subnet: row.subnet,
siteExitNodeId: row.siteExitNodeId
});
}
}
let siteResourcesWithFullDomain: { let siteResourcesWithFullDomain: {
siteResourceId: number; siteResourceId: number;
fullDomain: string | null; fullDomain: string | null;
mode: "http" | "host" | "cidr" | "ssh"; mode: "http" | "host" | "cidr";
}[] = []; }[] = [];
if (build == "enterprise") { if (build == "enterprise") {
// we dont want to do this on the cloud // we dont want to do this on the cloud
@@ -452,12 +324,6 @@ export async function getTraefikConfig(
domains.add(sr.fullDomain); domains.add(sr.fullDomain);
} }
} }
// Include browser gateway resource domains
for (const bgResource of browserGatewayResourcesMap.values()) {
if (bgResource.enabled && bgResource.ssl && bgResource.fullDomain) {
domains.add(bgResource.fullDomain);
}
}
// get the valid certs for these domains // get the valid certs for these domains
validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often
// logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`); // logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`);
@@ -723,7 +589,7 @@ export async function getTraefikConfig(
resource.ssl ? entrypointHttps : entrypointHttp resource.ssl ? entrypointHttps : entrypointHttp
], ],
service: maintenanceServiceName, service: maintenanceServiceName,
rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `, rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`,
priority: 2001, priority: 2001,
...(resource.ssl ? { tls } : {}) ...(resource.ssl ? { tls } : {})
}; };
@@ -964,7 +830,7 @@ export async function getTraefikConfig(
continue; continue;
} }
const protocol = resource.mode == "udp" ? "udp" : "tcp"; const protocol = resource.protocol.toLowerCase();
const port = resource.proxyPort; const port = resource.proxyPort;
if (!port) { if (!port) {
@@ -1059,267 +925,6 @@ export async function getTraefikConfig(
} }
} }
if (allowBrowserGatewayResources) {
// Generate Traefik config for browser gateway resources
const browserGatewayPort = 39999;
for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
if (!bgResource.enabled) continue;
if (!bgResource.domainId) continue;
if (!bgResource.fullDomain) continue;
if (!config_output.http.routers) config_output.http.routers = {};
if (!config_output.http.services) config_output.http.services = {};
const fullDomain = bgResource.fullDomain;
const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || [];
const routerMiddlewares = [
badgerMiddlewareName,
...additionalMiddlewares
];
const hostRule = `Host(\`${fullDomain}\`)`;
// Build TLS config
let tls = {};
if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
const domainParts = fullDomain.split(".");
let wildCard: string;
if (domainParts.length <= 2) {
wildCard = `*.${domainParts.join(".")}`;
} else {
wildCard = `*.${domainParts.slice(1).join(".")}`;
}
if (!bgResource.subdomain) {
wildCard = fullDomain;
}
const globalDefaultResolver =
config.getRawConfig().traefik.cert_resolver;
const globalDefaultPreferWildcard =
config.getRawConfig().traefik.prefer_wildcard_cert;
const resolverName = bgResource.domainCertResolver
? bgResource.domainCertResolver.trim()
: globalDefaultResolver;
const preferWildcard =
bgResource.preferWildcardCert !== undefined &&
bgResource.preferWildcardCert !== null
? bgResource.preferWildcardCert
: globalDefaultPreferWildcard;
tls = {
certResolver: resolverName,
...(preferWildcard ? { domains: [{ main: wildCard }] } : {})
};
} else {
const matchingCert = validCerts.find(
(cert) => cert.queriedDomain === fullDomain
);
if (!matchingCert) {
logger.debug(
`No matching certificate found for browser gateway domain: ${fullDomain}`
);
continue;
}
}
const bgUiServiceName = `bg-r${bgResource.resourceId}-ui-service`;
if (bgResource.ssl) {
const redirectRouterName = `bg-r${bgResource.resourceId}-redirect`;
config_output.http.routers![redirectRouterName] = {
entryPoints: [
config.getRawConfig().traefik.http_entrypoint
],
middlewares: [redirectHttpsMiddlewareName],
service: bgUiServiceName,
rule: hostRule,
priority: 100
};
}
// Collect online sites for this resource (for any type)
const anySiteOnline = bgResource.targets.some((t) => t.siteOnline);
// Maintenance page logic for browser gateway resources
let showBgMaintenancePage = false;
if (bgResource.maintenanceModeEnabled) {
if (bgResource.maintenanceModeType === "forced") {
showBgMaintenancePage = true;
} else if (bgResource.maintenanceModeType === "automatic") {
showBgMaintenancePage = !anySiteOnline;
}
}
if (showBgMaintenancePage && allowMaintenancePage) {
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
const entrypointHttp =
config.getRawConfig().traefik.http_entrypoint;
const entrypointHttps =
config.getRawConfig().traefik.https_entrypoint;
const maintenancePort = config.getRawConfig().server.next_port;
const maintenanceHost =
config.getRawConfig().server.internal_hostname;
if (!config_output.http.services)
config_output.http.services = {};
if (!config_output.http.middlewares)
config_output.http.middlewares = {};
if (!config_output.http.routers)
config_output.http.routers = {};
config_output.http.services![bgMaintenanceServiceName] = {
loadBalancer: {
servers: [
{
url: `http://${maintenanceHost}:${maintenancePort}`
}
],
passHostHeader: true
}
};
config_output.http.middlewares![bgRewriteMiddlewareName] = {
replacePathRegex: {
regex: "^/(.*)",
replacement: "/maintenance-screen"
}
};
config_output.http.routers![bgMaintenanceRouterName] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
middlewares: [bgRewriteMiddlewareName],
rule: hostRule,
priority: 2000,
...(bgResource.ssl ? { tls } : {})
};
config_output.http.routers![
`${bgMaintenanceRouterName}-assets`
] = {
entryPoints: [
bgResource.ssl ? entrypointHttps : entrypointHttp
],
service: bgMaintenanceServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 2001,
...(bgResource.ssl ? { tls } : {})
};
continue;
}
// Group targets by type and generate per-type websocket routers and services
const typeMap = new Map<string, typeof bgResource.targets>();
for (const t of bgResource.targets) {
if (!typeMap.has(t.bgType)) typeMap.set(t.bgType, []);
typeMap.get(t.bgType)!.push(t);
}
for (const [bgType, typedTargets] of typeMap.entries()) {
const bgKey = `bg-r${bgResource.resourceId}-${bgType}`;
const bgRouterName = `${bgKey}-router`;
const bgServiceName = `${bgKey}-service`;
const bgRule = `${hostRule} && PathPrefix(\`/gateway/${bgType}\`)`;
const servers = typedTargets
.filter((t) => {
if (!t.siteOnline && anySiteOnline) return false;
if (t.siteType === "newt") return !!t.subnet;
return false; // browser gateway only supported on newt sites
})
.map((t) => ({
url: `http://${t.subnet!.split("/")[0]}:${browserGatewayPort}`
}))
.filter(
(v, i, a) => a.findIndex((u) => u.url === v.url) === i
);
config_output.http.routers![bgRouterName] = {
entryPoints: [
bgResource.ssl
? config.getRawConfig().traefik.https_entrypoint
: config.getRawConfig().traefik.http_entrypoint
],
middlewares: routerMiddlewares,
service: bgServiceName,
rule: bgRule,
priority: 110, // highest - websocket path takes precedence
...(bgResource.ssl ? { tls } : {})
};
config_output.http.services![bgServiceName] = {
loadBalancer: {
servers
}
};
}
// UI: serve the browser gateway page from the internal pangolin instance.
// The primary type is used for the path rewrite (e.g. /rdp), mirroring
// how the maintenance page rewrites everything to /maintenance-screen.
const primaryType = typeMap.keys().next().value as string;
const internalHost = config.getRawConfig().server.internal_hostname;
const internalPort = config.getRawConfig().server.next_port;
const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`;
const entrypoint = bgResource.ssl
? config.getRawConfig().traefik.https_entrypoint
: config.getRawConfig().traefik.http_entrypoint;
if (!config_output.http.middlewares) {
config_output.http.middlewares = {};
}
config_output.http.middlewares![uiRewriteMiddlewareName] = {
replacePathRegex: {
regex: "^/(.*)",
replacement: `/${primaryType}`
}
};
config_output.http.services![bgUiServiceName] = {
loadBalancer: {
servers: [
{
url: `http://${internalHost}:${internalPort}`
}
]
}
};
// Assets router at higher priority so /_next files load without rewrite
config_output.http.routers![
`bg-r${bgResource.resourceId}-assets-router`
] = {
entryPoints: [entrypoint],
middlewares: routerMiddlewares,
service: bgUiServiceName,
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 101,
...(bgResource.ssl ? { tls } : {})
};
// Catch-all router rewrites everything on the domain to /{primaryType}
config_output.http.routers![
`bg-r${bgResource.resourceId}-ui-router`
] = {
entryPoints: [entrypoint],
middlewares: [...routerMiddlewares, uiRewriteMiddlewareName],
service: bgUiServiceName,
rule: hostRule,
priority: 100,
...(bgResource.ssl ? { tls } : {})
};
}
}
// Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that // Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that
// Traefik generates TLS certificates for those domains even when no // Traefik generates TLS certificates for those domains even when no
// matching resource exists yet. // matching resource exists yet.
@@ -1435,7 +1040,7 @@ export async function getTraefikConfig(
config_output.http.routers[`${siteResourceRouterName}-assets`] = { config_output.http.routers[`${siteResourceRouterName}-assets`] = {
entryPoints: [config.getRawConfig().traefik.https_entrypoint], entryPoints: [config.getRawConfig().traefik.https_entrypoint],
service: siteResourceServiceName, service: siteResourceServiceName,
rule: `Host(\`${fullDomain}\`) && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, rule: `Host(\`${fullDomain}\`) && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`,
priority: 101, priority: 101,
tls tls
}; };
@@ -1538,7 +1143,7 @@ export async function getTraefikConfig(
config.getRawConfig().traefik.https_entrypoint config.getRawConfig().traefik.https_entrypoint
], ],
service: "landing-service", service: "landing-service",
rule: `Host(\`${fullDomain}\`) && (PathRegexp(\`^/auth/resource/[^/]+$\`) || PathRegexp(\`^/auth/idp/[0-9]+/oidc/callback\`) || PathPrefix(\`/_next\`) || Path(\`/auth/org\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, rule: `Host(\`${fullDomain}\`) && (PathRegexp(\`^/auth/resource/[^/]+$\`) || PathRegexp(\`^/auth/idp/[0-9]+/oidc/callback\`) || PathPrefix(\`/_next\`) || Path(\`/auth/org\`) || PathRegexp(\`^/__nextjs*\`))`,
priority: 203, priority: 203,
tls: tls tls: tls
}; };
@@ -202,22 +202,7 @@ registry.registerPath({
} }
} }
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function createAlertRule( export async function createAlertRule(
@@ -38,22 +38,7 @@ registry.registerPath({
request: { request: {
params: paramsSchema params: paramsSchema
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function deleteAlertRule( export async function deleteAlertRule(
@@ -49,22 +49,7 @@ registry.registerPath({
request: { request: {
params: paramsSchema params: paramsSchema
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function getAlertRule( export async function getAlertRule(
@@ -95,22 +95,7 @@ registry.registerPath({
query: querySchema, query: querySchema,
params: paramsSchema params: paramsSchema
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function listAlertRules( export async function listAlertRules(
@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db"; import { db } from "@server/db";
import { import {
alertRules, alertRules,
@@ -149,10 +148,6 @@ const bodySchema = z
export type UpdateAlertRuleResponse = { export type UpdateAlertRuleResponse = {
alertRuleId: number; alertRuleId: number;
}; };
const UpdateAlertRuleResponseDataSchema = z.object({
alertRuleId: z.number()
});
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
@@ -169,16 +164,7 @@ registry.registerPath({
} }
} }
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateAlertRuleResponseDataSchema)
}
}
}
}
}); });
export async function updateAlertRule( export async function updateAlertRule(
@@ -24,7 +24,7 @@ import type { NextFunction, Request, Response } from "express";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string(), orgId: z.string(),
approvalId: z.coerce.number().int().positive() approvalId: z.string().transform(Number).pipe(z.int().positive())
}); });
const bodySchema = z.strictObject({ const bodySchema = z.strictObject({
@@ -18,7 +18,6 @@ import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { z } from "zod";
import logger from "@server/logger"; import logger from "@server/logger";
import { import {
queryAccessAuditLogsParams, queryAccessAuditLogsParams,
@@ -38,22 +37,7 @@ registry.registerPath({
query: queryAccessAuditLogsQuery, query: queryAccessAuditLogsQuery,
params: queryAccessAuditLogsParams params: queryAccessAuditLogsParams
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function exportAccessAuditLogs( export async function exportAccessAuditLogs(
@@ -18,7 +18,6 @@ import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { z } from "zod";
import logger from "@server/logger"; import logger from "@server/logger";
import { import {
queryActionAuditLogsParams, queryActionAuditLogsParams,
@@ -38,22 +37,7 @@ registry.registerPath({
query: queryActionAuditLogsQuery, query: queryActionAuditLogsQuery,
params: queryActionAuditLogsParams params: queryActionAuditLogsParams
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function exportActionAuditLogs( export async function exportActionAuditLogs(
@@ -18,7 +18,6 @@ import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { z } from "zod";
import logger from "@server/logger"; import logger from "@server/logger";
import { import {
queryConnectionAuditLogsParams, queryConnectionAuditLogsParams,
@@ -38,22 +37,7 @@ registry.registerPath({
query: queryConnectionAuditLogsQuery, query: queryConnectionAuditLogsQuery,
params: queryConnectionAuditLogsParams params: queryConnectionAuditLogsParams
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function exportConnectionAuditLogs( export async function exportConnectionAuditLogs(
@@ -93,20 +93,6 @@ export const queryAccessAuditLogsCombined = queryAccessAuditLogsQuery.merge(
); );
type Q = z.infer<typeof queryAccessAuditLogsCombined>; type Q = z.infer<typeof queryAccessAuditLogsCombined>;
function sortNamedFilterOptions<T extends { id: number; name: string | null }>(
items: T[]
): T[] {
return [...items].sort((a, b) => {
const nameA = a.name ?? "";
const nameB = b.name ?? "";
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return a.id - b.id;
});
}
function getWhere(data: Q) { function getWhere(data: Q) {
return and( return and(
gt(accessAuditLog.timestamp, data.timeStart), gt(accessAuditLog.timestamp, data.timeStart),
@@ -322,7 +308,7 @@ async function queryUniqueFilterAttributes(
actors: uniqueActors actors: uniqueActors
.map((row) => row.actor) .map((row) => row.actor)
.filter((actor): actor is string => actor !== null), .filter((actor): actor is string => actor !== null),
resources: sortNamedFilterOptions(resourcesWithNames), resources: resourcesWithNames,
locations: uniqueLocations locations: uniqueLocations
.map((row) => row.locations) .map((row) => row.locations)
.filter((location): location is string => location !== null) .filter((location): location is string => location !== null)
@@ -338,22 +324,7 @@ registry.registerPath({
query: queryAccessAuditLogsQuery, query: queryAccessAuditLogsQuery,
params: queryAccessAuditLogsParams params: queryAccessAuditLogsParams
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function queryAccessAuditLogs( export async function queryAccessAuditLogs(
@@ -165,22 +165,7 @@ registry.registerPath({
query: queryActionAuditLogsQuery, query: queryActionAuditLogsQuery,
params: queryActionAuditLogsParams params: queryActionAuditLogsParams
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function queryActionAuditLogs( export async function queryActionAuditLogs(
@@ -107,20 +107,6 @@ export const queryConnectionAuditLogsCombined =
queryConnectionAuditLogsQuery.merge(queryConnectionAuditLogsParams); queryConnectionAuditLogsQuery.merge(queryConnectionAuditLogsParams);
type Q = z.infer<typeof queryConnectionAuditLogsCombined>; type Q = z.infer<typeof queryConnectionAuditLogsCombined>;
function sortNamedFilterOptions<T extends { id: number; name: string | null }>(
items: T[]
): T[] {
return [...items].sort((a, b) => {
const nameA = a.name ?? "";
const nameB = b.name ?? "";
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return a.id - b.id;
});
}
function getWhere(data: Q) { function getWhere(data: Q) {
return and( return and(
gt(connectionAuditLog.startedAt, data.timeStart), gt(connectionAuditLog.startedAt, data.timeStart),
@@ -439,7 +425,7 @@ async function queryUniqueFilterAttributes(
.map((row) => row.destAddr) .map((row) => row.destAddr)
.filter((addr): addr is string => addr !== null), .filter((addr): addr is string => addr !== null),
clients: clientsWithNames, clients: clientsWithNames,
resources: sortNamedFilterOptions(resourcesWithNames), resources: resourcesWithNames,
users: usersWithEmails users: usersWithEmails
}; };
} }
@@ -453,22 +439,7 @@ registry.registerPath({
query: queryConnectionAuditLogsQuery, query: queryConnectionAuditLogsQuery,
params: queryConnectionAuditLogsParams params: queryConnectionAuditLogsParams
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function queryConnectionAuditLogs( export async function queryConnectionAuditLogs(
@@ -308,8 +308,8 @@ async function disableFeature(
await disableAutoProvisioning(orgId); await disableAutoProvisioning(orgId);
break; break;
case TierFeature.AdvancedPrivateResources: case TierFeature.SshPam:
await disableAdvancedPrivateResources(orgId); await disableSshPam(orgId);
break; break;
case TierFeature.FullRbac: case TierFeature.FullRbac:
@@ -357,11 +357,10 @@ async function disableDeviceApprovals(orgId: string): Promise<void> {
logger.info(`Disabled device approvals on all roles for org ${orgId}`); logger.info(`Disabled device approvals on all roles for org ${orgId}`);
} }
async function disableAdvancedPrivateResources(orgId: string): Promise<void> { async function disableSshPam(orgId: string): Promise<void> {
// TODO: implement logic to disable advanced private resourcs like ssh and ssh pam logger.info(
// logger.info( `Disabled SSH PAM options on all roles and site resources for org ${orgId}`
// `Disabled advanced private resources on all roles and site resources for org ${orgId}` );
// );
} }
async function disableFullRbac(orgId: string): Promise<void> { async function disableFullRbac(orgId: string): Promise<void> {
+1 -16
View File
@@ -39,22 +39,7 @@ const getOrgSchema = z.strictObject({
// request: { // request: {
// params: getOrgSchema // params: getOrgSchema
// }, // },
// responses: { // responses: {}
// 200: {
// description: "Successful response",
// content: {
// "application/json": {
// schema: z.object({
// data: z.unknown().nullable(),
// success: z.boolean(),
// error: z.boolean(),
// message: z.string(),
// status: z.number()
// })
// }
// }
// }
// }
// }); // });
export async function getOrgUsage( export async function getOrgUsage(
@@ -1,187 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
browserGatewayTarget,
BrowserGatewayTarget,
db,
newts,
resources,
sites
} from "@server/db";
import { eq, and } from "drizzle-orm";
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 { OpenAPITags, registry } from "@server/openApi";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { sendBrowserGatewayTargets } from "@server/routers/newt/targets";
import { generateId } from "@server/auth/sessions/app";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
});
const bodySchema = z.strictObject({
siteId: z.number().int().positive(),
type: z.enum(["ssh", "rdp", "vnc"]),
destination: z.string().nonempty(),
destinationPort: z.number().int().min(1).max(65535)
});
export type CreateBrowserGatewayTargetResponse = BrowserGatewayTarget;
registry.registerPath({
method: "put",
path: "/org/{orgId}/resource/{resourceId}/browser-gateway-target",
description: "Create a browser gateway target for a resource.",
tags: [OpenAPITags.Org],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
export async function createBrowserGatewayTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, resourceId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { siteId, type, destination, destinationPort } = parsedBody.data;
const [resource] = await db
.select()
.from(resources)
.where(
and(
eq(resources.resourceId, resourceId),
eq(resources.orgId, orgId)
)
)
.limit(1);
if (!resource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found in organization ${orgId}`
)
);
}
const [site] = await db
.select()
.from(sites)
.where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)))
.limit(1);
if (!site) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${siteId} not found in organization ${orgId}`
)
);
}
const plainToken = generateId(48);
const encryptedToken = encrypt(
plainToken,
config.getRawConfig().server.secret!
);
const [record] = await db
.insert(browserGatewayTarget)
.values({
resourceId,
siteId,
type,
destination,
destinationPort,
authToken: encryptedToken
})
.returning();
if (site.type === "newt") {
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, siteId))
.limit(1);
if (newt) {
await sendBrowserGatewayTargets(
newt.newtId,
[record],
newt.version
);
}
}
logger.info(
`Created browser gateway target ${record.browserGatewayTargetId} for resource ${resourceId}`
);
return response<CreateBrowserGatewayTargetResponse>(res, {
data: record,
success: true,
error: false,
message: "Browser gateway target created successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create browser gateway target"
)
);
}
}
@@ -1,130 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { browserGatewayTarget, db, newts, sites } from "@server/db";
import { eq, and } from "drizzle-orm";
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 { OpenAPITags, registry } from "@server/openApi";
import { removeBrowserGatewayTarget } from "@server/routers/newt/targets";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
browserGatewayTargetId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
});
registry.registerPath({
method: "delete",
path: "/org/{orgId}/browser-gateway-target/{browserGatewayTargetId}",
description: "Delete a browser gateway target.",
tags: [OpenAPITags.Org],
request: {
params: paramsSchema
},
responses: {}
});
export async function deleteBrowserGatewayTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, browserGatewayTargetId } = parsedParams.data;
const [existing] = await db
.select({ bgt: browserGatewayTarget, site: sites })
.from(browserGatewayTarget)
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
.where(
and(
eq(
browserGatewayTarget.browserGatewayTargetId,
browserGatewayTargetId
),
eq(sites.orgId, orgId)
)
)
.limit(1);
if (!existing) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Browser gateway target with ID ${browserGatewayTargetId} not found`
)
);
}
await db
.delete(browserGatewayTarget)
.where(
eq(
browserGatewayTarget.browserGatewayTargetId,
browserGatewayTargetId
)
);
if (existing.site.type === "newt") {
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, existing.bgt.siteId))
.limit(1);
if (newt) {
await removeBrowserGatewayTarget(
newt.newtId,
browserGatewayTargetId,
newt.version
);
}
}
logger.info(`Deleted browser gateway target ${browserGatewayTargetId}`);
return response(res, {
data: null,
success: true,
error: false,
message: "Browser gateway target deleted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to delete browser gateway target"
)
);
}
}
@@ -1,109 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
browserGatewayTarget,
BrowserGatewayTarget,
db,
sites
} from "@server/db";
import { eq, and } from "drizzle-orm";
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 { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
browserGatewayTargetId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
});
export type GetBrowserGatewayTargetResponse = BrowserGatewayTarget;
registry.registerPath({
method: "get",
path: "/org/{orgId}/browser-gateway-target/{browserGatewayTargetId}",
description: "Get a browser gateway target.",
tags: [OpenAPITags.Org],
request: {
params: paramsSchema
},
responses: {}
});
export async function getBrowserGatewayTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, browserGatewayTargetId } = parsedParams.data;
const [result] = await db
.select({ bgt: browserGatewayTarget })
.from(browserGatewayTarget)
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
.where(
and(
eq(
browserGatewayTarget.browserGatewayTargetId,
browserGatewayTargetId
),
eq(sites.orgId, orgId)
)
)
.limit(1);
if (!result) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Browser gateway target with ID ${browserGatewayTargetId} not found`
)
);
}
return response<GetBrowserGatewayTargetResponse>(res, {
data: result.bgt,
success: true,
error: false,
message: "Browser gateway target retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to retrieve browser gateway target"
)
);
}
}
@@ -1,112 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { browserGatewayTarget, db } from "@server/db";
import { resources, targets } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
const getBrowserTargetSchema = z
.object({
fullDomain: z.string().min(1, "fullDomain is required")
})
.strict();
export async function getBrowserTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsed = getBrowserTargetSchema.safeParse(req.query);
if (!parsed.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsed.error).toString()
)
);
}
const { fullDomain } = parsed.data;
logger.info(`Retrieving browser target for domain: ${fullDomain}`);
const [browserTarget] = await db
.select({
destination: browserGatewayTarget.destination,
destinationPort: browserGatewayTarget.destinationPort,
authToken: browserGatewayTarget.authToken,
resourceId: resources.resourceId,
niceId: resources.niceId,
orgId: resources.orgId,
pamMode: resources.pamMode,
authDaemonMode: resources.authDaemonMode
})
.from(browserGatewayTarget)
.innerJoin(
resources,
eq(browserGatewayTarget.resourceId, resources.resourceId)
)
.where(eq(resources.fullDomain, fullDomain))
.limit(1);
const decryptedAuthToken = decrypt(
browserTarget.authToken,
config.getRawConfig().server.secret!
);
if (!browserTarget) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"No resource found for this domain"
)
);
}
return response<GetBrowserTargetResponse>(res, {
data: {
ip: browserTarget.destination,
port: browserTarget.destinationPort,
authToken: decryptedAuthToken,
pamMode: browserTarget.pamMode,
authDaemonMode: browserTarget.authDaemonMode,
orgId: browserTarget.orgId,
resourceId: browserTarget.resourceId,
niceId: browserTarget.niceId
},
success: true,
error: false,
message: "Browser target retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred while retrieving the browser target"
)
);
}
}
@@ -1,19 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
export * from "./createBrowserGatewayTarget";
export * from "./updateBrowserGatewayTarget";
export * from "./deleteBrowserGatewayTarget";
export * from "./getBrowserGatewayTarget";
export * from "./listBrowserGatewayTargets";
export * from "./getBrowserTarget";
@@ -1,159 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
browserGatewayTarget,
BrowserGatewayTarget,
db,
resources,
sites
} from "@server/db";
import { eq, and } from "drizzle-orm";
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 { OpenAPITags, registry } from "@server/openApi";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
resourceId: z.string().transform(Number).pipe(z.number().int().positive())
});
const querySchema = z.object({
limit: z
.string()
.optional()
.default("1000")
.transform(Number)
.pipe(z.number().int().positive()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.number().int().nonnegative())
});
export type ListBrowserGatewayTargetsResponse = {
targets: BrowserGatewayTarget[];
total: number;
limit: number;
offset: number;
};
registry.registerPath({
method: "get",
path: "/org/{orgId}/resource/{resourceId}/browser-gateway-targets",
description: "List browser gateway targets for a resource.",
tags: [OpenAPITags.Org],
request: {
params: paramsSchema,
query: querySchema
},
responses: {}
});
export async function listBrowserGatewayTargets(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, resourceId } = parsedParams.data;
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { limit, offset } = parsedQuery.data;
const [resource] = await db
.select()
.from(resources)
.where(
and(
eq(resources.resourceId, resourceId),
eq(resources.orgId, orgId)
)
)
.limit(1);
if (!resource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Resource with ID ${resourceId} not found in organization ${orgId}`
)
);
}
const rows = await db
.select({
browserGatewayTargetId:
browserGatewayTarget.browserGatewayTargetId,
resourceId: browserGatewayTarget.resourceId,
siteId: browserGatewayTarget.siteId,
authToken: browserGatewayTarget.authToken,
type: browserGatewayTarget.type,
destination: browserGatewayTarget.destination,
destinationPort: browserGatewayTarget.destinationPort,
siteName: sites.name
})
.from(browserGatewayTarget)
.leftJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
.where(eq(browserGatewayTarget.resourceId, resourceId))
.limit(limit)
.offset(offset);
return response<ListBrowserGatewayTargetsResponse>(res, {
data: {
targets: rows as any,
total: rows.length,
limit,
offset
},
success: true,
error: false,
message: "Browser gateway targets retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to list browser gateway targets"
)
);
}
}
@@ -1,180 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
browserGatewayTarget,
BrowserGatewayTarget,
db,
newts,
sites
} from "@server/db";
import { eq, and } from "drizzle-orm";
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 { OpenAPITags, registry } from "@server/openApi";
import { sendBrowserGatewayTargets } from "@server/routers/newt/targets";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty(),
browserGatewayTargetId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
});
const bodySchema = z.strictObject({
siteId: z.number().int().positive().optional(),
type: z.enum(["ssh", "rdp", "vnc"]).optional(),
destination: z.string().nonempty().optional(),
destinationPort: z.number().int().min(1).max(65535).optional()
});
export type UpdateBrowserGatewayTargetResponse = BrowserGatewayTarget;
registry.registerPath({
method: "post",
path: "/org/{orgId}/browser-gateway-target/{browserGatewayTargetId}",
description: "Update a browser gateway target.",
tags: [OpenAPITags.Org],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
export async function updateBrowserGatewayTarget(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, browserGatewayTargetId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { siteId, type, destination, destinationPort } = parsedBody.data;
const [existing] = await db
.select({ bgt: browserGatewayTarget, site: sites })
.from(browserGatewayTarget)
.innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId))
.where(
and(
eq(
browserGatewayTarget.browserGatewayTargetId,
browserGatewayTargetId
),
eq(sites.orgId, orgId)
)
)
.limit(1);
if (!existing) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Browser gateway target with ID ${browserGatewayTargetId} not found`
)
);
}
const updateValues: Partial<BrowserGatewayTarget> = {};
if (siteId !== undefined) updateValues.siteId = siteId;
if (type !== undefined) updateValues.type = type;
if (destination !== undefined) updateValues.destination = destination;
if (destinationPort !== undefined)
updateValues.destinationPort = destinationPort;
const [updated] = await db
.update(browserGatewayTarget)
.set(updateValues)
.where(
eq(
browserGatewayTarget.browserGatewayTargetId,
browserGatewayTargetId
)
)
.returning();
const targetSiteId = siteId ?? existing.bgt.siteId;
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, targetSiteId))
.limit(1);
if (site && site.type === "newt") {
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, targetSiteId))
.limit(1);
if (newt) {
await sendBrowserGatewayTargets(
newt.newtId,
[updated],
newt.version
);
}
}
logger.info(`Updated browser gateway target ${browserGatewayTargetId}`);
return response<UpdateBrowserGatewayTargetResponse>(res, {
data: updated,
success: true,
error: false,
message: "Browser gateway target updated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to update browser gateway target"
)
);
}
}
@@ -115,22 +115,7 @@ registry.registerPath({
orgId: z.string() orgId: z.string()
}) })
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function getCertificate( export async function getCertificate(
@@ -25,7 +25,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
const restartCertificateParamsSchema = z.strictObject({ const restartCertificateParamsSchema = z.strictObject({
certId: z.coerce.number().int().positive(), certId: z.string().transform(stoi).pipe(z.int().positive()),
orgId: z.string() orgId: z.string()
}); });
@@ -36,26 +36,11 @@ registry.registerPath({
tags: ["Certificate"], tags: ["Certificate"],
request: { request: {
params: z.object({ params: z.object({
certId: z.coerce.number().int().positive(), certId: z.string().transform(stoi).pipe(z.int().positive()),
orgId: z.string() orgId: z.string()
}) })
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function restartCertificate( export async function restartCertificate(
@@ -42,22 +42,7 @@ registry.registerPath({
params: paramsSchema, params: paramsSchema,
query: querySchema query: querySchema
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function checkDomainNamespaceAvailability( export async function checkDomainNamespaceAvailability(
@@ -25,7 +25,6 @@ import { OpenAPITags, registry } from "@server/openApi";
import { isSubscribed } from "#private/lib/isSubscribed"; import { isSubscribed } from "#private/lib/isSubscribed";
import { build } from "@server/build"; import { build } from "@server/build";
import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const paramsSchema = z.strictObject({}); const paramsSchema = z.strictObject({});
@@ -66,20 +65,6 @@ export type ListDomainNamespacesResponse = {
pagination: { total: number; limit: number; offset: number }; pagination: { total: number; limit: number; offset: number };
}; };
const ListDomainNamespacesResponseDataSchema = z.object({
domainNamespaces: z.array(
z.object({
domainNamespaceId: z.string(),
domainId: z.string()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
registry.registerPath({ registry.registerPath({
method: "get", method: "get",
path: "/domains/namepaces", path: "/domains/namepaces",
@@ -88,18 +73,7 @@ registry.registerPath({
request: { request: {
query: querySchema query: querySchema
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
ListDomainNamespacesResponseDataSchema
)
}
}
}
}
}); });
export async function listDomainNamespaces( export async function listDomainNamespaces(
@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db"; import { db } from "@server/db";
import { eventStreamingDestinations } from "@server/db"; import { eventStreamingDestinations } from "@server/db";
import { logStreamingManager } from "#private/lib/logStreaming"; import { logStreamingManager } from "#private/lib/logStreaming";
@@ -43,10 +42,6 @@ const bodySchema = z.strictObject({
export type CreateEventStreamingDestinationResponse = { export type CreateEventStreamingDestinationResponse = {
destinationId: number; destinationId: number;
}; };
const CreateEventStreamingDestinationResponseDataSchema = z.object({
destinationId: z.number()
});
registry.registerPath({ registry.registerPath({
method: "put", method: "put",
@@ -63,16 +58,7 @@ registry.registerPath({
} }
} }
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(CreateEventStreamingDestinationResponseDataSchema)
}
}
}
}
}); });
export async function createEventStreamingDestination( export async function createEventStreamingDestination(
@@ -38,22 +38,7 @@ registry.registerPath({
request: { request: {
params: paramsSchema params: paramsSchema
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function deleteEventStreamingDestination( export async function deleteEventStreamingDestination(
@@ -24,7 +24,6 @@ import { OpenAPITags, registry } from "@server/openApi";
import { eq, sql } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import { decrypt } from "@server/lib/crypto"; import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
const paramsSchema = z.strictObject({ const paramsSchema = z.strictObject({
orgId: z.string().nonempty() orgId: z.string().nonempty()
@@ -68,31 +67,6 @@ export type ListEventStreamingDestinationsResponse = {
}; };
}; };
const ListEventStreamingDestinationsResponseDataSchema = z.object({
destinations: z.array(
z.object({
destinationId: z.number(),
orgId: z.string(),
type: z.string(),
config: z.string(),
enabled: z.boolean(),
lastError: z.string().nullable(),
lastErrorAt: z.number().nullable(),
createdAt: z.number(),
updatedAt: z.number(),
sendConnectionLogs: z.boolean(),
sendRequestLogs: z.boolean(),
sendActionLogs: z.boolean(),
sendAccessLogs: z.boolean()
})
),
pagination: z.object({
total: z.number(),
limit: z.number(),
offset: z.number()
})
});
async function query(orgId: string, limit: number, offset: number) { async function query(orgId: string, limit: number, offset: number) {
const res = await db const res = await db
.select() .select()
@@ -114,18 +88,7 @@ registry.registerPath({
query: querySchema, query: querySchema,
params: paramsSchema params: paramsSchema
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(
ListEventStreamingDestinationsResponseDataSchema
)
}
}
}
}
}); });
export async function listEventStreamingDestinations( export async function listEventStreamingDestinations(
@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db } from "@server/db"; import { db } from "@server/db";
import { eventStreamingDestinations } from "@server/db"; import { eventStreamingDestinations } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
@@ -46,10 +45,6 @@ const bodySchema = z.strictObject({
export type UpdateEventStreamingDestinationResponse = { export type UpdateEventStreamingDestinationResponse = {
destinationId: number; destinationId: number;
}; };
const UpdateEventStreamingDestinationResponseDataSchema = z.object({
destinationId: z.number()
});
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
@@ -66,16 +61,7 @@ registry.registerPath({
} }
} }
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateEventStreamingDestinationResponseDataSchema)
}
}
}
}
}); });
export async function updateEventStreamingDestination( export async function updateEventStreamingDestination(
+2 -84
View File
@@ -31,11 +31,8 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination"; import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import * as alertRule from "#private/routers/alertRule"; import * as alertRule from "#private/routers/alertRule";
import * as healthChecks from "#private/routers/healthChecks"; import * as healthChecks from "#private/routers/healthChecks";
import * as browserGatewayTarget from "#private/routers/browserGatewayTarget";
import * as labels from "#private/routers/labels"; import * as labels from "#private/routers/labels";
import * as client from "@server/routers/client"; import * as client from "@server/routers/client";
import * as resource from "#private/routers/resource";
import * as policy from "#private/routers/policy";
import { import {
verifyOrgAccess, verifyOrgAccess,
@@ -49,8 +46,7 @@ import {
verifyUserCanSetUserOrgRoles, verifyUserCanSetUserOrgRoles,
verifySiteProvisioningKeyAccess, verifySiteProvisioningKeyAccess,
verifyIsLoggedInUser, verifyIsLoggedInUser,
verifyAdmin, verifyAdmin
verifyResourcePolicyAccess
} from "@server/middlewares"; } from "@server/middlewares";
import { ActionsEnum } from "@server/auth/actions"; import { ActionsEnum } from "@server/auth/actions";
import { import {
@@ -388,39 +384,6 @@ authenticated.get(
approval.countApprovals approval.countApprovals
); );
authenticated.delete(
"/resource-policy/:resourcePolicyId",
verifyResourcePolicyAccess,
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyLimits,
verifyUserHasAction(ActionsEnum.deleteResourcePolicy),
logActionAudit(ActionsEnum.deleteResourcePolicy),
policy.deleteResourcePolicy
);
authenticated.get(
"/org/:orgId/resource-policies",
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.listResourcePolicies),
logActionAudit(ActionsEnum.listResourcePolicies),
policy.listResourcePolicies
);
authenticated.post(
"/org/:orgId/resource-policy",
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createResourcePolicy),
logActionAudit(ActionsEnum.createResourcePolicy),
policy.createResourcePolicy
);
authenticated.put( authenticated.put(
"/org/:orgId/approvals/:approvalId", "/org/:orgId/approvals/:approvalId",
verifyValidLicense, verifyValidLicense,
@@ -610,7 +573,7 @@ authenticated.put(
authenticated.post( authenticated.post(
"/org/:orgId/ssh/sign-key", "/org/:orgId/ssh/sign-key",
verifyValidLicense, verifyValidLicense,
verifyValidSubscription(tierMatrix.advancedPrivateResources), verifyValidSubscription(tierMatrix.sshPam),
verifyOrgAccess, verifyOrgAccess,
verifyLimits, verifyLimits,
verifyUserHasAction(ActionsEnum.signSshKey), verifyUserHasAction(ActionsEnum.signSshKey),
@@ -879,48 +842,3 @@ authenticated.post(
verifyClientAccess, verifyClientAccess,
client.rebuildClientAssociationsCacheRoute client.rebuildClientAssociationsCacheRoute
); );
authenticated.put(
"/org/:orgId/resource/:resourceId/browser-gateway-target",
verifyValidLicense,
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createBrowserGatewayTarget),
logActionAudit(ActionsEnum.createBrowserGatewayTarget),
browserGatewayTarget.createBrowserGatewayTarget
);
authenticated.get(
"/org/:orgId/resource/:resourceId/browser-gateway-targets",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listBrowserGatewayTargets),
browserGatewayTarget.listBrowserGatewayTargets
);
authenticated.get(
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getBrowserGatewayTarget),
browserGatewayTarget.getBrowserGatewayTarget
);
authenticated.post(
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
verifyValidLicense,
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.updateBrowserGatewayTarget),
logActionAudit(ActionsEnum.updateBrowserGatewayTarget),
browserGatewayTarget.updateBrowserGatewayTarget
);
authenticated.delete(
"/org/:orgId/browser-gateway-target/:browserGatewayTargetId",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.deleteBrowserGatewayTarget),
logActionAudit(ActionsEnum.deleteBrowserGatewayTarget),
browserGatewayTarget.deleteBrowserGatewayTarget
);
@@ -13,7 +13,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema";
import { db, targetHealthCheck, newts, sites } from "@server/db"; import { db, targetHealthCheck, newts, sites } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
@@ -53,10 +52,6 @@ const bodySchema = z.strictObject({
export type CreateHealthCheckResponse = { export type CreateHealthCheckResponse = {
targetHealthCheckId: number; targetHealthCheckId: number;
}; };
const CreateHealthCheckResponseDataSchema = z.object({
targetHealthCheckId: z.number()
});
registry.registerPath({ registry.registerPath({
method: "put", method: "put",
@@ -73,16 +68,7 @@ registry.registerPath({
} }
} }
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(CreateHealthCheckResponseDataSchema)
}
}
}
}
}); });
export async function createHealthCheck( export async function createHealthCheck(
@@ -41,22 +41,7 @@ registry.registerPath({
request: { request: {
params: paramsSchema params: paramsSchema
}, },
responses: { responses: {}
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.unknown().nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
}); });
export async function deleteHealthCheck( export async function deleteHealthCheck(

Some files were not shown because too many files have changed in this diff Show More