mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-31 17:50:47 +02:00
Compare commits
27 Commits
1.19.2
...
c76b4555e1
| Author | SHA1 | Date | |
|---|---|---|---|
| c76b4555e1 | |||
| c25bfbad27 | |||
| 44782f8963 | |||
| e6f7cd6da9 | |||
| 62c7e0a13e | |||
| 753358a17d | |||
| c859393418 | |||
| d747b45f0b | |||
| a24091257a | |||
| 16c0f4eef4 | |||
| a08c6d70fe | |||
| a6568692b7 | |||
| a1196d3da6 | |||
| 70bc4c0b30 | |||
| a0fef89031 | |||
| ea1badf4e0 | |||
| f15654ed11 | |||
| 4435a669a6 | |||
| 0b41fe3d49 | |||
| 90eceb457a | |||
| f39cbc9bf4 | |||
| 50da863bb7 | |||
| c6ddd5c402 | |||
| 0fb5ace9c7 | |||
| b136bd2246 | |||
| 92d611df9a | |||
| 44c16d69af |
+19
-29
@@ -1,52 +1,42 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 1
|
||||||
groups:
|
groups:
|
||||||
dev-patch-updates:
|
npm-dependencies:
|
||||||
dependency-type: "development"
|
patterns:
|
||||||
update-types:
|
- "*"
|
||||||
- "patch"
|
|
||||||
dev-minor-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
prod-patch-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
prod-minor-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 1
|
||||||
groups:
|
groups:
|
||||||
patch-updates:
|
docker-dependencies:
|
||||||
update-types:
|
patterns:
|
||||||
- "patch"
|
- "*"
|
||||||
minor-updates:
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 1
|
||||||
|
groups:
|
||||||
|
github-actions-dependencies:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
directory: "/install"
|
directory: "/install"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 1
|
||||||
groups:
|
groups:
|
||||||
patch-updates:
|
go-install-dependencies:
|
||||||
update-types:
|
patterns:
|
||||||
- "patch"
|
- "*"
|
||||||
minor-updates:
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
@@ -256,7 +256,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: get-tag
|
id: get-tag
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Build Docker image sqlite
|
- name: Build Docker image sqlite
|
||||||
run: make dev-build-sqlite
|
run: make dev-build-sqlite
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Build Docker image pg
|
- name: Build Docker image pg
|
||||||
run: make dev-build-pg
|
run: make dev-build-pg
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/charmbracelet/huh v1.0.0
|
github.com/charmbracelet/huh v1.0.0
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
golang.org/x/term v0.43.0
|
golang.org/x/term v0.44.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,6 +33,6 @@ require (
|
|||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.44.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-4
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
|||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
|||||||
Generated
+74
-8
@@ -7296,6 +7296,72 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
|
||||||
@@ -12191,16 +12257,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "4.0.5",
|
"version": "4.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
"hasown": "^2.0.2",
|
"hasown": "^2.0.4",
|
||||||
"mime-types": "^2.1.12"
|
"mime-types": "^2.1.35"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -12629,9 +12695,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
const MAX_RECURSION_DEPTH = 100;
|
||||||
|
|
||||||
|
const segmentRegexCache = new Map<string, RegExp>();
|
||||||
|
|
||||||
|
function getSegmentRegex(patternPart: string): RegExp {
|
||||||
|
let regex = segmentRegexCache.get(patternPart);
|
||||||
|
if (!regex) {
|
||||||
|
const regexPattern = patternPart
|
||||||
|
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
||||||
|
.replace(/\*/g, ".*")
|
||||||
|
.replace(/\?/g, ".");
|
||||||
|
regex = new RegExp(`^${regexPattern}$`);
|
||||||
|
segmentRegexCache.set(patternPart, regex);
|
||||||
|
}
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPathAllowed(pattern: string, path: string): boolean {
|
||||||
|
const normalize = (p: string) => p.split("/").filter(Boolean);
|
||||||
|
const patternParts = normalize(pattern);
|
||||||
|
const pathParts = normalize(path);
|
||||||
|
|
||||||
|
function matchSegments(
|
||||||
|
patternIndex: number,
|
||||||
|
pathIndex: number,
|
||||||
|
depth: number = 0
|
||||||
|
): boolean {
|
||||||
|
if (depth > MAX_RECURSION_DEPTH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPatternPart = patternParts[patternIndex];
|
||||||
|
const currentPathPart = pathParts[pathIndex];
|
||||||
|
|
||||||
|
if (patternIndex >= patternParts.length) {
|
||||||
|
return pathIndex >= pathParts.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathIndex >= pathParts.length) {
|
||||||
|
return patternParts.slice(patternIndex).every((p) => p === "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPatternPart === "*") {
|
||||||
|
if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPatternPart.includes("*")) {
|
||||||
|
const regex = getSegmentRegex(currentPatternPart);
|
||||||
|
|
||||||
|
if (regex.test(currentPathPart)) {
|
||||||
|
return matchSegments(
|
||||||
|
patternIndex + 1,
|
||||||
|
pathIndex + 1,
|
||||||
|
depth + 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPatternPart !== currentPathPart) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchSegments(0, 0, 0);
|
||||||
|
}
|
||||||
@@ -511,6 +511,12 @@ export class TraefikConfigManager {
|
|||||||
let traefikConfig;
|
let traefikConfig;
|
||||||
try {
|
try {
|
||||||
const currentExitNode = await getCurrentExitNodeId();
|
const currentExitNode = await getCurrentExitNodeId();
|
||||||
|
|
||||||
|
const maintenancePort = config.getRawConfig().server.next_port;
|
||||||
|
const maintenanceHost =
|
||||||
|
config.getRawConfig().server.internal_hostname;
|
||||||
|
const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
|
||||||
|
|
||||||
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
||||||
traefikConfig = await getTraefikConfig(
|
traefikConfig = await getTraefikConfig(
|
||||||
// this is called by the local exit node to get its own config
|
// this is called by the local exit node to get its own config
|
||||||
@@ -521,7 +527,8 @@ export class TraefikConfigManager {
|
|||||||
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
|
pangolinUIUrl, // generate maintenance pages on cloud and hybrid
|
||||||
|
pangolinUIUrl // generate browser gateway targets on cloud and hybrid
|
||||||
);
|
);
|
||||||
|
|
||||||
const domains = new Set<string>();
|
const domains = new Set<string>();
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ export async function getTraefikConfig(
|
|||||||
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
|
filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE
|
||||||
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
|
generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE
|
||||||
allowRawResources = true,
|
allowRawResources = true,
|
||||||
allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE
|
maintenancePageUiUrl: string | null = null, // UNUSED BUT USED IN PRIVATE
|
||||||
allowBrowserGatewayResources = true
|
browserGatewayUiUrl: string | null = null // UNUSED BUT USED IN PRIVATE
|
||||||
): 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
|
||||||
|
|||||||
@@ -84,8 +84,8 @@ export async function getTraefikConfig(
|
|||||||
filterOutNamespaceDomains = false,
|
filterOutNamespaceDomains = false,
|
||||||
generateLoginPageRouters = false,
|
generateLoginPageRouters = false,
|
||||||
allowRawResources = true,
|
allowRawResources = true,
|
||||||
allowMaintenancePage = true,
|
maintenancePageUiUrl: string | null = null,
|
||||||
allowBrowserGatewayResources = true
|
browserGatewayUiUrl: string | null = null
|
||||||
): 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
|
||||||
@@ -317,7 +317,7 @@ export async function getTraefikConfig(
|
|||||||
BrowserGatewayResourceEntry
|
BrowserGatewayResourceEntry
|
||||||
>();
|
>();
|
||||||
|
|
||||||
if (allowBrowserGatewayResources) {
|
if (browserGatewayUiUrl) {
|
||||||
for (const row of resourcesWithTargetsAndSites) {
|
for (const row of resourcesWithTargetsAndSites) {
|
||||||
if (!["ssh", "vnc", "rdp"].includes(row.mode)) {
|
if (!["ssh", "vnc", "rdp"].includes(row.mode)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -630,10 +630,11 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showMaintenancePage && allowMaintenancePage) {
|
if (showMaintenancePage && maintenancePageUiUrl) {
|
||||||
const maintenanceServiceName = `${key}-maintenance-service`;
|
const maintenanceServiceName = `${key}-maintenance-service`;
|
||||||
const maintenanceRouterName = `${key}-maintenance-router`;
|
const maintenanceRouterName = `${key}-maintenance-router`;
|
||||||
const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
|
const rewriteMiddlewareName = `${key}-maintenance-rewrite`;
|
||||||
|
const maintenanceHeadersMiddlewareName = `${key}-maintenance-headers`;
|
||||||
|
|
||||||
const entrypointHttp =
|
const entrypointHttp =
|
||||||
config.getRawConfig().traefik.http_entrypoint;
|
config.getRawConfig().traefik.http_entrypoint;
|
||||||
@@ -646,15 +647,11 @@ export async function getTraefikConfig(
|
|||||||
? `*.${domainParts.slice(1).join(".")}`
|
? `*.${domainParts.slice(1).join(".")}`
|
||||||
: fullDomain;
|
: fullDomain;
|
||||||
|
|
||||||
const maintenancePort = config.getRawConfig().server.next_port;
|
|
||||||
const maintenanceHost =
|
|
||||||
config.getRawConfig().server.internal_hostname;
|
|
||||||
|
|
||||||
config_output.http.services[maintenanceServiceName] = {
|
config_output.http.services[maintenanceServiceName] = {
|
||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: `http://${maintenanceHost}:${maintenancePort}`
|
url: maintenancePageUiUrl
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
passHostHeader: true
|
passHostHeader: true
|
||||||
@@ -673,12 +670,26 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config_output.http.middlewares[
|
||||||
|
maintenanceHeadersMiddlewareName
|
||||||
|
] = {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: {
|
||||||
|
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
|
||||||
|
"p-host": fullDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
config_output.http.routers[maintenanceRouterName] = {
|
config_output.http.routers[maintenanceRouterName] = {
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
resource.ssl ? entrypointHttps : entrypointHttp
|
resource.ssl ? entrypointHttps : entrypointHttp
|
||||||
],
|
],
|
||||||
service: maintenanceServiceName,
|
service: maintenanceServiceName,
|
||||||
middlewares: [rewriteMiddlewareName],
|
middlewares: [
|
||||||
|
rewriteMiddlewareName,
|
||||||
|
maintenanceHeadersMiddlewareName
|
||||||
|
],
|
||||||
rule: rule,
|
rule: rule,
|
||||||
priority: 2000,
|
priority: 2000,
|
||||||
...(resource.ssl ? { tls } : {})
|
...(resource.ssl ? { tls } : {})
|
||||||
@@ -691,6 +702,7 @@ export async function getTraefikConfig(
|
|||||||
resource.ssl ? entrypointHttps : entrypointHttp
|
resource.ssl ? entrypointHttps : entrypointHttp
|
||||||
],
|
],
|
||||||
service: maintenanceServiceName,
|
service: maintenanceServiceName,
|
||||||
|
middlewares: [maintenanceHeadersMiddlewareName],
|
||||||
rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `,
|
rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `,
|
||||||
priority: 2001,
|
priority: 2001,
|
||||||
...(resource.ssl ? { tls } : {})
|
...(resource.ssl ? { tls } : {})
|
||||||
@@ -1027,7 +1039,7 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowBrowserGatewayResources) {
|
if (browserGatewayUiUrl) {
|
||||||
// Generate Traefik config for browser gateway resources
|
// Generate Traefik config for browser gateway resources
|
||||||
const browserGatewayPort = 39999;
|
const browserGatewayPort = 39999;
|
||||||
for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
|
for (const [, bgResource] of browserGatewayResourcesMap.entries()) {
|
||||||
@@ -1119,20 +1131,17 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showBgMaintenancePage && allowMaintenancePage) {
|
if (showBgMaintenancePage && maintenancePageUiUrl) {
|
||||||
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
|
const bgMaintenanceServiceName = `bg-r${bgResource.resourceId}-maintenance-service`;
|
||||||
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
|
const bgMaintenanceRouterName = `bg-r${bgResource.resourceId}-maintenance-router`;
|
||||||
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
|
const bgRewriteMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-rewrite`;
|
||||||
|
const bgMaintenanceHeadersMiddlewareName = `bg-r${bgResource.resourceId}-maintenance-headers`;
|
||||||
|
|
||||||
const entrypointHttp =
|
const entrypointHttp =
|
||||||
config.getRawConfig().traefik.http_entrypoint;
|
config.getRawConfig().traefik.http_entrypoint;
|
||||||
const entrypointHttps =
|
const entrypointHttps =
|
||||||
config.getRawConfig().traefik.https_entrypoint;
|
config.getRawConfig().traefik.https_entrypoint;
|
||||||
|
|
||||||
const maintenancePort = config.getRawConfig().server.next_port;
|
|
||||||
const maintenanceHost =
|
|
||||||
config.getRawConfig().server.internal_hostname;
|
|
||||||
|
|
||||||
if (!config_output.http.services)
|
if (!config_output.http.services)
|
||||||
config_output.http.services = {};
|
config_output.http.services = {};
|
||||||
if (!config_output.http.middlewares)
|
if (!config_output.http.middlewares)
|
||||||
@@ -1144,7 +1153,7 @@ export async function getTraefikConfig(
|
|||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: `http://${maintenanceHost}:${maintenancePort}`
|
url: maintenancePageUiUrl
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
passHostHeader: true
|
passHostHeader: true
|
||||||
@@ -1158,12 +1167,26 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config_output.http.middlewares![
|
||||||
|
bgMaintenanceHeadersMiddlewareName
|
||||||
|
] = {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: {
|
||||||
|
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
|
||||||
|
"p-host": fullDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
config_output.http.routers![bgMaintenanceRouterName] = {
|
config_output.http.routers![bgMaintenanceRouterName] = {
|
||||||
entryPoints: [
|
entryPoints: [
|
||||||
bgResource.ssl ? entrypointHttps : entrypointHttp
|
bgResource.ssl ? entrypointHttps : entrypointHttp
|
||||||
],
|
],
|
||||||
service: bgMaintenanceServiceName,
|
service: bgMaintenanceServiceName,
|
||||||
middlewares: [bgRewriteMiddlewareName],
|
middlewares: [
|
||||||
|
bgRewriteMiddlewareName,
|
||||||
|
bgMaintenanceHeadersMiddlewareName
|
||||||
|
],
|
||||||
rule: hostRule,
|
rule: hostRule,
|
||||||
priority: 2000,
|
priority: 2000,
|
||||||
...(bgResource.ssl ? { tls } : {})
|
...(bgResource.ssl ? { tls } : {})
|
||||||
@@ -1176,6 +1199,7 @@ export async function getTraefikConfig(
|
|||||||
bgResource.ssl ? entrypointHttps : entrypointHttp
|
bgResource.ssl ? entrypointHttps : entrypointHttp
|
||||||
],
|
],
|
||||||
service: bgMaintenanceServiceName,
|
service: bgMaintenanceServiceName,
|
||||||
|
middlewares: [bgMaintenanceHeadersMiddlewareName],
|
||||||
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
|
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
|
||||||
priority: 2001,
|
priority: 2001,
|
||||||
...(bgResource.ssl ? { tls } : {})
|
...(bgResource.ssl ? { tls } : {})
|
||||||
@@ -1234,9 +1258,8 @@ export async function getTraefikConfig(
|
|||||||
// The primary type is used for the path rewrite (e.g. /rdp), mirroring
|
// The primary type is used for the path rewrite (e.g. /rdp), mirroring
|
||||||
// how the maintenance page rewrites everything to /maintenance-screen.
|
// how the maintenance page rewrites everything to /maintenance-screen.
|
||||||
const primaryType = typeMap.keys().next().value as string;
|
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 uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`;
|
||||||
|
const uiHeadersMiddlewareName = `bg-r${bgResource.resourceId}-ui-headers`;
|
||||||
const entrypoint = bgResource.ssl
|
const entrypoint = bgResource.ssl
|
||||||
? config.getRawConfig().traefik.https_entrypoint
|
? config.getRawConfig().traefik.https_entrypoint
|
||||||
: config.getRawConfig().traefik.http_entrypoint;
|
: config.getRawConfig().traefik.http_entrypoint;
|
||||||
@@ -1252,22 +1275,33 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config_output.http.middlewares![uiHeadersMiddlewareName] = {
|
||||||
|
headers: {
|
||||||
|
customRequestHeaders: {
|
||||||
|
Host: "app.pangolin.net", // if we are sending to the cloud the host needs to be this but we will pull the p-host to find the resource
|
||||||
|
"p-host": fullDomain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
config_output.http.services![bgUiServiceName] = {
|
config_output.http.services![bgUiServiceName] = {
|
||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: `http://${internalHost}:${internalPort}`
|
url: browserGatewayUiUrl
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Assets router at higher priority so /_next files load without rewrite
|
// Assets router at higher priority so /_next files load without rewrite.
|
||||||
|
// Do NOT apply the path-rewrite middleware here — static assets must
|
||||||
|
// keep their original path; only the host headers are needed.
|
||||||
config_output.http.routers![
|
config_output.http.routers![
|
||||||
`bg-r${bgResource.resourceId}-assets-router`
|
`bg-r${bgResource.resourceId}-assets-router`
|
||||||
] = {
|
] = {
|
||||||
entryPoints: [entrypoint],
|
entryPoints: [entrypoint],
|
||||||
middlewares: routerMiddlewares,
|
middlewares: [...routerMiddlewares, uiHeadersMiddlewareName],
|
||||||
service: bgUiServiceName,
|
service: bgUiServiceName,
|
||||||
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
|
rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
|
||||||
priority: 101,
|
priority: 101,
|
||||||
@@ -1279,7 +1313,11 @@ export async function getTraefikConfig(
|
|||||||
`bg-r${bgResource.resourceId}-ui-router`
|
`bg-r${bgResource.resourceId}-ui-router`
|
||||||
] = {
|
] = {
|
||||||
entryPoints: [entrypoint],
|
entryPoints: [entrypoint],
|
||||||
middlewares: [...routerMiddlewares, uiRewriteMiddlewareName],
|
middlewares: [
|
||||||
|
...routerMiddlewares,
|
||||||
|
uiRewriteMiddlewareName,
|
||||||
|
uiHeadersMiddlewareName
|
||||||
|
],
|
||||||
service: bgUiServiceName,
|
service: bgUiServiceName,
|
||||||
rule: hostRule,
|
rule: hostRule,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
@@ -1312,10 +1350,6 @@ export async function getTraefikConfig(
|
|||||||
const siteResourceRouterName = `${srKey}-router`;
|
const siteResourceRouterName = `${srKey}-router`;
|
||||||
const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`;
|
const siteResourceRewriteMiddlewareName = `${srKey}-rewrite`;
|
||||||
|
|
||||||
const maintenancePort = config.getRawConfig().server.next_port;
|
|
||||||
const maintenanceHost =
|
|
||||||
config.getRawConfig().server.internal_hostname;
|
|
||||||
|
|
||||||
if (!config_output.http.routers) {
|
if (!config_output.http.routers) {
|
||||||
config_output.http.routers = {};
|
config_output.http.routers = {};
|
||||||
}
|
}
|
||||||
@@ -1331,7 +1365,7 @@ export async function getTraefikConfig(
|
|||||||
loadBalancer: {
|
loadBalancer: {
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: `http://${maintenanceHost}:${maintenancePort}`
|
url: maintenancePageUiUrl
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
passHostHeader: true
|
passHostHeader: true
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ hybridRouter.get(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pangolinUIUrl = config.getRawConfig().app.dashboard_url; // points to the dashboard to serve from there
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const traefikConfig = await getTraefikConfig(
|
const traefikConfig = await getTraefikConfig(
|
||||||
remoteExitNode.exitNodeId,
|
remoteExitNode.exitNodeId,
|
||||||
@@ -284,8 +286,8 @@ hybridRouter.get(
|
|||||||
true, // But don't allow domain namespace resources
|
true, // But don't allow domain namespace resources
|
||||||
false, // Dont include login pages,
|
false, // Dont include login pages,
|
||||||
true, // allow raw resources
|
true, // allow raw resources
|
||||||
false, // dont generate maintenance page
|
pangolinUIUrl, // dont generate maintenance page
|
||||||
false // dont generate browser gateway targets
|
pangolinUIUrl // generate browser gateway targets
|
||||||
);
|
);
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { assertEquals } from "@test/assert";
|
import { assertEquals } from "@test/assert";
|
||||||
import { REGIONS } from "@server/db/regions";
|
import { REGIONS } from "@server/db/regions";
|
||||||
|
import { isPathAllowed } from "@server/lib/pathMatch";
|
||||||
|
|
||||||
function isIpInRegion(
|
function isIpInRegion(
|
||||||
ipCountryCode: string | undefined,
|
ipCountryCode: string | undefined,
|
||||||
@@ -33,76 +34,6 @@ function isIpInRegion(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPathAllowed(pattern: string, path: string): boolean {
|
|
||||||
// Normalize and split paths into segments
|
|
||||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
|
||||||
const patternParts = normalize(pattern);
|
|
||||||
const pathParts = normalize(path);
|
|
||||||
|
|
||||||
// Recursive function to try different wildcard matches
|
|
||||||
function matchSegments(patternIndex: number, pathIndex: number): boolean {
|
|
||||||
const indent = " ".repeat(pathIndex); // Indent based on recursion depth
|
|
||||||
const currentPatternPart = patternParts[patternIndex];
|
|
||||||
const currentPathPart = pathParts[pathIndex];
|
|
||||||
|
|
||||||
// If we've consumed all pattern parts, we should have consumed all path parts
|
|
||||||
if (patternIndex >= patternParts.length) {
|
|
||||||
const result = pathIndex >= pathParts.length;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've consumed all path parts but still have pattern parts
|
|
||||||
if (pathIndex >= pathParts.length) {
|
|
||||||
// The only way this can match is if all remaining pattern parts are wildcards
|
|
||||||
const remainingPattern = patternParts.slice(patternIndex);
|
|
||||||
const result = remainingPattern.every((p) => p === "*");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For full segment wildcards, try consuming different numbers of path segments
|
|
||||||
if (currentPatternPart === "*") {
|
|
||||||
// Try consuming 0 segments (skip the wildcard)
|
|
||||||
if (matchSegments(patternIndex + 1, pathIndex)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try consuming current segment and recursively try rest
|
|
||||||
if (matchSegments(patternIndex, pathIndex + 1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix")
|
|
||||||
if (currentPatternPart.includes("*")) {
|
|
||||||
// Convert the pattern segment to a regex pattern
|
|
||||||
const regexPattern = currentPatternPart
|
|
||||||
.replace(/\*/g, ".*") // Replace * with .* for regex wildcard
|
|
||||||
.replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
|
|
||||||
|
|
||||||
const regex = new RegExp(`^${regexPattern}$`);
|
|
||||||
|
|
||||||
if (regex.test(currentPathPart)) {
|
|
||||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For regular segments, they must match exactly
|
|
||||||
if (currentPatternPart !== currentPathPart) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to next segments in both pattern and path
|
|
||||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = matchSegments(0, 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function runTests() {
|
function runTests() {
|
||||||
console.log("Running path matching tests...");
|
console.log("Running path matching tests...");
|
||||||
|
|
||||||
@@ -308,6 +239,121 @@ function runTests() {
|
|||||||
console.log("All path matching tests passed!");
|
console.log("All path matching tests passed!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runSpecialCharacterTests() {
|
||||||
|
console.log("\nRunning special character tests...");
|
||||||
|
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
isPathAllowed("(api*", "anything");
|
||||||
|
isPathAllowed("a(b*", "a(bc");
|
||||||
|
isPathAllowed("c[d*", "c[de");
|
||||||
|
isPathAllowed("x{2}*", "x{2}y");
|
||||||
|
isPathAllowed("a|b*", "a|bc");
|
||||||
|
isPathAllowed("back\\slash*", "back\\slashed");
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
console.error(
|
||||||
|
"Patterns accepted by isValidUrlGlobPattern crashed the matcher:",
|
||||||
|
e instanceof Error ? e.message : e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertEquals(
|
||||||
|
threw,
|
||||||
|
false,
|
||||||
|
"Patterns with regex metacharacters must not throw"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("(api*", "(api-v1"),
|
||||||
|
true,
|
||||||
|
"Parenthesis should be treated as a literal character"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("(api*", "xapi-v1"),
|
||||||
|
false,
|
||||||
|
"Parenthesis should not match other characters"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("a(b)*", "a(b)c"),
|
||||||
|
true,
|
||||||
|
"Parentheses pair should be treated as literal characters"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("*.png", "image.png"),
|
||||||
|
true,
|
||||||
|
"Dot should match a literal dot"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("*.png", "imageXpng"),
|
||||||
|
false,
|
||||||
|
"Dot should not act as a regex wildcard"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("v1.0*", "v1.0.1"),
|
||||||
|
true,
|
||||||
|
"Version-like literal should match itself"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("v1.0*", "v1x0-beta"),
|
||||||
|
false,
|
||||||
|
"Version-like literal should not match arbitrary characters"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("a+b*", "a+bc"),
|
||||||
|
true,
|
||||||
|
"Plus should be treated as a literal character"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("a+b*", "aaabc"),
|
||||||
|
false,
|
||||||
|
"Plus should not act as a regex quantifier"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("$ref*", "$refs"),
|
||||||
|
true,
|
||||||
|
"Dollar sign should be treated as a literal character"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("price$*", "price$100"),
|
||||||
|
true,
|
||||||
|
"Dollar sign mid-pattern should be treated as a literal character"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("^start*", "^started"),
|
||||||
|
true,
|
||||||
|
"Caret should be treated as a literal character"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("a|b*", "a|bc"),
|
||||||
|
true,
|
||||||
|
"Pipe should be treated as a literal character"
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("a|b*", "a"),
|
||||||
|
false,
|
||||||
|
"Pipe should not act as regex alternation"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("file?*", "fileX"),
|
||||||
|
true,
|
||||||
|
"Question mark should still act as a single-character wildcard"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
isPathAllowed("api/*", "api/" + "x/".repeat(50)),
|
||||||
|
true,
|
||||||
|
"Deeply nested paths should still match"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("All special character tests passed!");
|
||||||
|
}
|
||||||
|
|
||||||
function runRegionTests() {
|
function runRegionTests() {
|
||||||
console.log("\nRunning isIpInRegion tests...");
|
console.log("\nRunning isIpInRegion tests...");
|
||||||
|
|
||||||
@@ -367,6 +413,7 @@ function runRegionTests() {
|
|||||||
// Run all tests
|
// Run all tests
|
||||||
try {
|
try {
|
||||||
runTests();
|
runTests();
|
||||||
|
runSpecialCharacterTests();
|
||||||
runRegionTests();
|
runRegionTests();
|
||||||
console.log("\n✅ All tests passed!");
|
console.log("\n✅ All tests passed!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
import { isIpInCidr, stripPortFromHost } from "@server/lib/ip";
|
||||||
|
import { isPathAllowed } from "@server/lib/pathMatch";
|
||||||
import { response } from "@server/lib/response";
|
import { response } from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -1090,143 +1091,7 @@ async function checkRules(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPathAllowed(pattern: string, path: string): boolean {
|
export { isPathAllowed };
|
||||||
logger.debug(`\nMatching path "${path}" against pattern "${pattern}"`);
|
|
||||||
|
|
||||||
// Normalize and split paths into segments
|
|
||||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
|
||||||
const patternParts = normalize(pattern);
|
|
||||||
const pathParts = normalize(path);
|
|
||||||
|
|
||||||
logger.debug(`Normalized pattern parts: [${patternParts.join(", ")}]`);
|
|
||||||
logger.debug(`Normalized path parts: [${pathParts.join(", ")}]`);
|
|
||||||
|
|
||||||
// Maximum recursion depth to prevent stack overflow and memory issues
|
|
||||||
const MAX_RECURSION_DEPTH = 100;
|
|
||||||
|
|
||||||
// Recursive function to try different wildcard matches
|
|
||||||
function matchSegments(
|
|
||||||
patternIndex: number,
|
|
||||||
pathIndex: number,
|
|
||||||
depth: number = 0
|
|
||||||
): boolean {
|
|
||||||
// Check recursion depth limit
|
|
||||||
if (depth > MAX_RECURSION_DEPTH) {
|
|
||||||
logger.warn(
|
|
||||||
`Path matching exceeded maximum recursion depth (${MAX_RECURSION_DEPTH}) for pattern "${pattern}" and path "${path}"`
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const indent = " ".repeat(depth); // Indent based on recursion depth
|
|
||||||
const currentPatternPart = patternParts[patternIndex];
|
|
||||||
const currentPathPart = pathParts[pathIndex];
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Checking patternIndex=${patternIndex} (${currentPatternPart || "END"}) vs pathIndex=${pathIndex} (${currentPathPart || "END"}) [depth=${depth}]`
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we've consumed all pattern parts, we should have consumed all path parts
|
|
||||||
if (patternIndex >= patternParts.length) {
|
|
||||||
const result = pathIndex >= pathParts.length;
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Reached end of pattern, remaining path: ${pathParts.slice(pathIndex).join("/")} -> ${result}`
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've consumed all path parts but still have pattern parts
|
|
||||||
if (pathIndex >= pathParts.length) {
|
|
||||||
// The only way this can match is if all remaining pattern parts are wildcards
|
|
||||||
const remainingPattern = patternParts.slice(patternIndex);
|
|
||||||
const result = remainingPattern.every((p) => p === "*");
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Reached end of path, remaining pattern: ${remainingPattern.join("/")} -> ${result}`
|
|
||||||
);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For full segment wildcards, try consuming different numbers of path segments
|
|
||||||
if (currentPatternPart === "*") {
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Found wildcard at pattern index ${patternIndex}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Try consuming 0 segments (skip the wildcard)
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Trying to skip wildcard (consume 0 segments)`
|
|
||||||
);
|
|
||||||
if (matchSegments(patternIndex + 1, pathIndex, depth + 1)) {
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Successfully matched by skipping wildcard`
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try consuming current segment and recursively try rest
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Trying to consume segment "${currentPathPart}" for wildcard`
|
|
||||||
);
|
|
||||||
if (matchSegments(patternIndex, pathIndex + 1, depth + 1)) {
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Successfully matched by consuming segment for wildcard`
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(`${indent}Failed to match wildcard`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix")
|
|
||||||
if (currentPatternPart.includes("*")) {
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Found in-segment wildcard in "${currentPatternPart}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Convert the pattern segment to a regex pattern
|
|
||||||
const regexPattern = currentPatternPart
|
|
||||||
.replace(/\*/g, ".*") // Replace * with .* for regex wildcard
|
|
||||||
.replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
|
|
||||||
|
|
||||||
const regex = new RegExp(`^${regexPattern}$`);
|
|
||||||
|
|
||||||
if (regex.test(currentPathPart)) {
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Segment with wildcard matches: "${currentPatternPart}" matches "${currentPathPart}"`
|
|
||||||
);
|
|
||||||
return matchSegments(
|
|
||||||
patternIndex + 1,
|
|
||||||
pathIndex + 1,
|
|
||||||
depth + 1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Segment with wildcard mismatch: "${currentPatternPart}" doesn't match "${currentPathPart}"`
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For regular segments, they must match exactly
|
|
||||||
if (currentPatternPart !== currentPathPart) {
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Segment mismatch: "${currentPatternPart}" != "${currentPathPart}"`
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
`${indent}Segments match: "${currentPatternPart}" = "${currentPathPart}"`
|
|
||||||
);
|
|
||||||
// Move to next segments in both pattern and path
|
|
||||||
return matchSegments(patternIndex + 1, pathIndex + 1, depth + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = matchSegments(0, 0, 0);
|
|
||||||
logger.debug(`Final result: ${result}`);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isIpInGeoIP(
|
async function isIpInGeoIP(
|
||||||
ipCountryCode: string | undefined,
|
ipCountryCode: string | undefined,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
verifyApiKey,
|
verifyApiKey,
|
||||||
verifyApiKeyOrgAccess,
|
verifyApiKeyOrgAccess,
|
||||||
verifyApiKeyHasAction,
|
verifyApiKeyHasAction,
|
||||||
verifyApiKeyCanSetUserOrgRoles,
|
|
||||||
verifyApiKeySiteAccess,
|
verifyApiKeySiteAccess,
|
||||||
verifyApiKeyResourceAccess,
|
verifyApiKeyResourceAccess,
|
||||||
verifyApiKeyTargetAccess,
|
verifyApiKeyTargetAccess,
|
||||||
@@ -974,6 +973,13 @@ authenticated.get(
|
|||||||
idp.getIdp
|
idp.getIdp
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/idp/:idpId",
|
||||||
|
verifyApiKeyIsRoot,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.deleteIdp),
|
||||||
|
idp.deleteIdp
|
||||||
|
);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/idp/:idpId/org/:orgId",
|
"/idp/:idpId/org/:orgId",
|
||||||
verifyApiKeyIsRoot,
|
verifyApiKeyIsRoot,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
// TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly)
|
// TODO: somehow we should make sure a recent hole punch has happened if this occurs (hole punch could be from the last restart if done quickly)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 5) {
|
if (existingSite.lastHolePunch && now - existingSite.lastHolePunch > 12) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the site reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
|
`Site last hole punch is too old; skipping this register. The site is failing to hole punch and identify its network address with the server. Can the site reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
// this prevents us from accepting a register from an olm that has not hole punched yet.
|
// this prevents us from accepting a register from an olm that has not hole punched yet.
|
||||||
// the olm will pump the register so we can keep checking
|
// the olm will pump the register so we can keep checking
|
||||||
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
// TODO: I still think there is a better way to do this rather than locking it out here but ???
|
||||||
if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) {
|
if (now - (client.lastHolePunch || 0) > 12 && sitesCount > 0) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
|
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
|
||||||
{ orgId: client.orgId, clientId: client.clientId }
|
{ orgId: client.orgId, clientId: client.clientId }
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { response as sendResponse } from "@server/lib/response";
|
import { response as sendResponse } from "@server/lib/response";
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { APP_VERSION } from "@server/lib/consts";
|
import { APP_VERSION } from "@server/lib/consts";
|
||||||
import license from "#dynamic/license/license";
|
import license from "#dynamic/license/license";
|
||||||
@@ -22,9 +21,6 @@ export async function getServerInfo(
|
|||||||
next: NextFunction
|
next: NextFunction
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const supporterData = config.getSupporterData();
|
|
||||||
const supporterStatusValid = supporterData?.valid || false;
|
|
||||||
|
|
||||||
let enterpriseLicenseValid = false;
|
let enterpriseLicenseValid = false;
|
||||||
let enterpriseLicenseType: string | null = null;
|
let enterpriseLicenseType: string | null = null;
|
||||||
|
|
||||||
@@ -41,7 +37,7 @@ export async function getServerInfo(
|
|||||||
return sendResponse<GetServerInfoResponse>(res, {
|
return sendResponse<GetServerInfoResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
version: APP_VERSION,
|
version: APP_VERSION,
|
||||||
supporterStatusValid,
|
supporterStatusValid: true,
|
||||||
build,
|
build,
|
||||||
enterpriseLicenseValid,
|
enterpriseLicenseValid,
|
||||||
enterpriseLicenseType
|
enterpriseLicenseType
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ export async function traefikConfigProvider(
|
|||||||
// Get the current exit node name from config
|
// Get the current exit node name from config
|
||||||
const currentExitNodeId = await getCurrentExitNodeId();
|
const currentExitNodeId = await getCurrentExitNodeId();
|
||||||
|
|
||||||
|
const maintenancePort = config.getRawConfig().server.next_port;
|
||||||
|
const maintenanceHost = config.getRawConfig().server.internal_hostname;
|
||||||
|
const pangolinUIUrl = `http://${maintenanceHost}:${maintenancePort}`;
|
||||||
|
|
||||||
const traefikConfig = await getTraefikConfig(
|
const traefikConfig = await getTraefikConfig(
|
||||||
currentExitNodeId,
|
currentExitNodeId,
|
||||||
config.getRawConfig().traefik.site_types,
|
config.getRawConfig().traefik.site_types,
|
||||||
build == "oss", // filter out the namespace domains in open source
|
build == "oss", // filter out the namespace domains in open source
|
||||||
build != "oss", // generate the login pages on the cloud and and enterprise,
|
build != "oss", // generate the login pages on the cloud and and enterprise,
|
||||||
config.getRawConfig().traefik.allow_raw_resources,
|
config.getRawConfig().traefik.allow_raw_resources,
|
||||||
build != "oss" // generate browser gateway resources on cloud and enterprise
|
pangolinUIUrl,
|
||||||
|
pangolinUIUrl
|
||||||
);
|
);
|
||||||
|
|
||||||
if (traefikConfig?.http?.middlewares) {
|
if (traefikConfig?.http?.middlewares) {
|
||||||
|
|||||||
@@ -42,7 +42,14 @@ import {
|
|||||||
SettingsSectionFooter
|
SettingsSectionFooter
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { ArrowRight, Check, ExternalLink, Heart, InfoIcon, TicketCheck } from "lucide-react";
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Check,
|
||||||
|
ExternalLink,
|
||||||
|
Heart,
|
||||||
|
InfoIcon,
|
||||||
|
TicketCheck
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import DismissableBanner from "@app/components/DismissableBanner";
|
import DismissableBanner from "@app/components/DismissableBanner";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
@@ -50,7 +57,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|||||||
import { SitePriceCalculator } from "@app/components/SitePriceCalculator";
|
import { SitePriceCalculator } from "@app/components/SitePriceCalculator";
|
||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
// import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
const ENTERPRISE_DOCS_URL =
|
const ENTERPRISE_DOCS_URL =
|
||||||
@@ -82,7 +89,7 @@ export default function LicensePage() {
|
|||||||
const [isActivatingLicense, setIsActivatingLicense] = useState(false);
|
const [isActivatingLicense, setIsActivatingLicense] = useState(false);
|
||||||
const [isDeletingLicense, setIsDeletingLicense] = useState(false);
|
const [isDeletingLicense, setIsDeletingLicense] = useState(false);
|
||||||
const [isRecheckingLicense, setIsRecheckingLicense] = useState(false);
|
const [isRecheckingLicense, setIsRecheckingLicense] = useState(false);
|
||||||
const { supporterStatus } = useSupporterStatusContext();
|
// const { supporterStatus } = useSupporterStatusContext();
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -347,9 +354,7 @@ export default function LicensePage() {
|
|||||||
storageKey="license-banner-dismissed"
|
storageKey="license-banner-dismissed"
|
||||||
version={1}
|
version={1}
|
||||||
title={t("licenseBannerTitle")}
|
title={t("licenseBannerTitle")}
|
||||||
titleIcon={
|
titleIcon={<TicketCheck className="w-5 h-5 text-primary" />}
|
||||||
<TicketCheck className="w-5 h-5 text-primary" />
|
|
||||||
}
|
|
||||||
description={t("licenseBannerDescription")}
|
description={t("licenseBannerDescription")}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
+19
-19
@@ -68,15 +68,15 @@ export default async function RootLayout({
|
|||||||
const env = pullEnv();
|
const env = pullEnv();
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
|
|
||||||
const supporterData = {
|
// const supporterData = {
|
||||||
visible: true
|
// visible: true
|
||||||
} as any;
|
// } as any;
|
||||||
|
|
||||||
const res = await priv.get<AxiosResponse<IsSupporterKeyVisibleResponse>>(
|
// const res = await priv.get<AxiosResponse<IsSupporterKeyVisibleResponse>>(
|
||||||
"supporter-key/visible"
|
// "supporter-key/visible"
|
||||||
);
|
// );
|
||||||
supporterData.visible = res.data.data.visible;
|
// supporterData.visible = res.data.data.visible;
|
||||||
supporterData.tier = res.data.data.tier;
|
// supporterData.tier = res.data.data.tier;
|
||||||
|
|
||||||
let licenseStatus: GetLicenseStatusResponse;
|
let licenseStatus: GetLicenseStatusResponse;
|
||||||
if (build === "enterprise") {
|
if (build === "enterprise") {
|
||||||
@@ -127,20 +127,20 @@ export default async function RootLayout({
|
|||||||
<LicenseStatusProvider
|
<LicenseStatusProvider
|
||||||
licenseStatus={licenseStatus}
|
licenseStatus={licenseStatus}
|
||||||
>
|
>
|
||||||
<SupportStatusProvider
|
{/* <SupportStatusProvider
|
||||||
supporterStatus={supporterData}
|
supporterStatus={supporterData}
|
||||||
>
|
> */}
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<SplashImage>
|
<SplashImage>
|
||||||
<LicenseViolation />
|
|
||||||
{children}
|
|
||||||
</SplashImage>
|
|
||||||
<LicenseViolation />
|
<LicenseViolation />
|
||||||
</div>
|
{children}
|
||||||
|
</SplashImage>
|
||||||
|
<LicenseViolation />
|
||||||
</div>
|
</div>
|
||||||
</SupportStatusProvider>
|
</div>
|
||||||
|
{/* </SupportStatusProvider> */}
|
||||||
</LicenseStatusProvider>
|
</LicenseStatusProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</TanstackQueryProvider>
|
</TanstackQueryProvider>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default async function MaintenanceScreen() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const headersList = await headers();
|
const headersList = await headers();
|
||||||
const host = headersList.get("host") || "";
|
const host = headersList.get("p-host") || headersList.get("host") || "";
|
||||||
const hostname = host.split(":")[0];
|
const hostname = host.split(":")[0];
|
||||||
|
|
||||||
const res = await priv.get<AxiosResponse<GetMaintenanceInfoResponse>>(
|
const res = await priv.get<AxiosResponse<GetMaintenanceInfoResponse>>(
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
// import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export default function AuthPageFooterNotices() {
|
export default function AuthPageFooterNotices() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { supporterStatus } = useSupporterStatusContext();
|
// const { supporterStatus } = useSupporterStatusContext();
|
||||||
const { isUnlocked, licenseStatus } = useLicenseStatusContext();
|
const { isUnlocked, licenseStatus } = useLicenseStatusContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{supporterStatus?.visible && (
|
{/* {supporterStatus?.visible && (
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
<span className="text-sm text-muted-foreground opacity-50">
|
<span className="text-sm text-muted-foreground opacity-50">
|
||||||
{t("noSupportKey")}
|
{t("noSupportKey")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
{build === "enterprise" && !isUnlocked() ? (
|
{build === "enterprise" && !isUnlocked() ? (
|
||||||
<div className="text-center mt-2">
|
<div className="text-center mt-2">
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
|
|||||||
@@ -9,33 +9,34 @@ export default function SupporterMessage({ tier }: { tier: string }) {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center space-x-2 whitespace-nowrap group">
|
<></>
|
||||||
<span
|
// <div className="relative flex items-center space-x-2 whitespace-nowrap group">
|
||||||
className="cursor-pointer"
|
// <span
|
||||||
onClick={(e) => {
|
// className="cursor-pointer"
|
||||||
// Get the bounding box of the element
|
// onClick={(e) => {
|
||||||
const rect = (
|
// // Get the bounding box of the element
|
||||||
e.target as HTMLElement
|
// const rect = (
|
||||||
).getBoundingClientRect();
|
// e.target as HTMLElement
|
||||||
|
// ).getBoundingClientRect();
|
||||||
|
|
||||||
// Trigger confetti centered on the word "Pangolin"
|
// // Trigger confetti centered on the word "Pangolin"
|
||||||
confetti({
|
// confetti({
|
||||||
particleCount: 100,
|
// particleCount: 100,
|
||||||
spread: 70,
|
// spread: 70,
|
||||||
origin: {
|
// origin: {
|
||||||
x: (rect.left + rect.width / 2) / window.innerWidth,
|
// x: (rect.left + rect.width / 2) / window.innerWidth,
|
||||||
y: rect.top / window.innerHeight
|
// y: rect.top / window.innerHeight
|
||||||
},
|
// },
|
||||||
colors: ["#FFA500", "#FF4500", "#FFD700"]
|
// colors: ["#FFA500", "#FF4500", "#FFD700"]
|
||||||
});
|
// });
|
||||||
}}
|
// }}
|
||||||
>
|
// >
|
||||||
Pangolin
|
// Pangolin
|
||||||
</span>
|
// </span>
|
||||||
<Star className="w-3 h-3" />
|
// <Star className="w-3 h-3" />
|
||||||
<div className="absolute left-1/2 transform -translate-x-1/2 -top-10 hidden group-hover:block text-primary text-sm rounded-md border shadow-md px-4 py-2 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
// <div className="absolute left-1/2 transform -translate-x-1/2 -top-10 hidden group-hover:block text-primary text-sm rounded-md border shadow-md px-4 py-2 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
{t("componentsSupporterMessage", { tier: tier })}
|
// {t("componentsSupporterMessage", { tier: tier })}
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-110
@@ -3,7 +3,7 @@
|
|||||||
// THIS IS DEPRECATED AND IS NO LONGER SHOWED TO THE USER WITH THE DISCONTINUATION
|
// THIS IS DEPRECATED AND IS NO LONGER SHOWED TO THE USER WITH THE DISCONTINUATION
|
||||||
// OF THE SUPPORTER PROGRAM. IT MAY BE REMOVED IN A FUTURE UPDATE.
|
// OF THE SUPPORTER PROGRAM. IT MAY BE REMOVED IN A FUTURE UPDATE.
|
||||||
|
|
||||||
import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
// import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -58,134 +58,134 @@ interface SupporterStatusProps {
|
|||||||
export default function SupporterStatus({
|
export default function SupporterStatus({
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
}: SupporterStatusProps) {
|
}: SupporterStatusProps) {
|
||||||
const { supporterStatus, updateSupporterStatus } =
|
// const { supporterStatus, updateSupporterStatus } =
|
||||||
useSupporterStatusContext();
|
// useSupporterStatusContext();
|
||||||
const [supportOpen, setSupportOpen] = useState(false);
|
// const [supportOpen, setSupportOpen] = useState(false);
|
||||||
const [keyOpen, setKeyOpen] = useState(false);
|
// const [keyOpen, setKeyOpen] = useState(false);
|
||||||
const [purchaseOptionsOpen, setPurchaseOptionsOpen] = useState(false);
|
// const [purchaseOptionsOpen, setPurchaseOptionsOpen] = useState(false);
|
||||||
|
|
||||||
const { env } = useEnvContext();
|
// const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
// const api = createApiClient({ env });
|
||||||
const t = useTranslations();
|
// const t = useTranslations();
|
||||||
|
|
||||||
const formSchema = z.object({
|
// const formSchema = z.object({
|
||||||
githubUsername: z.string().nonempty({
|
// githubUsername: z.string().nonempty({
|
||||||
error: "GitHub username is required"
|
// error: "GitHub username is required"
|
||||||
}),
|
// }),
|
||||||
key: z.string().nonempty({
|
// key: z.string().nonempty({
|
||||||
error: "Supporter key is required"
|
// error: "Supporter key is required"
|
||||||
})
|
// })
|
||||||
});
|
// });
|
||||||
|
|
||||||
const form = useForm({
|
// const form = useForm({
|
||||||
resolver: zodResolver(formSchema),
|
// resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
// defaultValues: {
|
||||||
githubUsername: "",
|
// githubUsername: "",
|
||||||
key: ""
|
// key: ""
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
async function hide() {
|
// async function hide() {
|
||||||
await api.post("/supporter-key/hide");
|
// await api.post("/supporter-key/hide");
|
||||||
|
|
||||||
updateSupporterStatus({
|
// updateSupporterStatus({
|
||||||
visible: false
|
// visible: false
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
// async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
try {
|
// try {
|
||||||
const res = await api.post<
|
// const res = await api.post<
|
||||||
AxiosResponse<ValidateSupporterKeyResponse>
|
// AxiosResponse<ValidateSupporterKeyResponse>
|
||||||
>("/supporter-key/validate", {
|
// >("/supporter-key/validate", {
|
||||||
githubUsername: values.githubUsername,
|
// githubUsername: values.githubUsername,
|
||||||
key: values.key
|
// key: values.key
|
||||||
});
|
// });
|
||||||
|
|
||||||
const data = res.data.data;
|
// const data = res.data.data;
|
||||||
|
|
||||||
if (!data || !data.valid) {
|
// if (!data || !data.valid) {
|
||||||
toast({
|
// toast({
|
||||||
variant: "destructive",
|
// variant: "destructive",
|
||||||
title: t("supportKeyInvalid"),
|
// title: t("supportKeyInvalid"),
|
||||||
description: t("supportKeyInvalidDescription")
|
// description: t("supportKeyInvalidDescription")
|
||||||
});
|
// });
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Trigger the toast
|
// // Trigger the toast
|
||||||
toast({
|
// toast({
|
||||||
variant: "default",
|
// variant: "default",
|
||||||
title: t("supportKeyValid"),
|
// title: t("supportKeyValid"),
|
||||||
description: t("supportKeyValidDescription")
|
// description: t("supportKeyValidDescription")
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Fireworks-style confetti
|
// // Fireworks-style confetti
|
||||||
const duration = 5 * 1000; // 5 seconds
|
// const duration = 5 * 1000; // 5 seconds
|
||||||
const animationEnd = Date.now() + duration;
|
// const animationEnd = Date.now() + duration;
|
||||||
const defaults = {
|
// const defaults = {
|
||||||
startVelocity: 30,
|
// startVelocity: 30,
|
||||||
spread: 360,
|
// spread: 360,
|
||||||
ticks: 60,
|
// ticks: 60,
|
||||||
zIndex: 0,
|
// zIndex: 0,
|
||||||
colors: ["#FFA500", "#FF4500", "#FFD700"] // Orange hues
|
// colors: ["#FFA500", "#FF4500", "#FFD700"] // Orange hues
|
||||||
};
|
// };
|
||||||
|
|
||||||
function randomInRange(min: number, max: number) {
|
// function randomInRange(min: number, max: number) {
|
||||||
return Math.random() * (max - min) + min;
|
// return Math.random() * (max - min) + min;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
// const interval = setInterval(() => {
|
||||||
const timeLeft = animationEnd - Date.now();
|
// const timeLeft = animationEnd - Date.now();
|
||||||
|
|
||||||
if (timeLeft <= 0) {
|
// if (timeLeft <= 0) {
|
||||||
clearInterval(interval);
|
// clearInterval(interval);
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const particleCount = 50 * (timeLeft / duration);
|
// const particleCount = 50 * (timeLeft / duration);
|
||||||
|
|
||||||
// Launch confetti from two random horizontal positions
|
// // Launch confetti from two random horizontal positions
|
||||||
confetti({
|
// confetti({
|
||||||
...defaults,
|
// ...defaults,
|
||||||
particleCount,
|
// particleCount,
|
||||||
origin: {
|
// origin: {
|
||||||
x: randomInRange(0.1, 0.3),
|
// x: randomInRange(0.1, 0.3),
|
||||||
y: Math.random() - 0.2
|
// y: Math.random() - 0.2
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
confetti({
|
// confetti({
|
||||||
...defaults,
|
// ...defaults,
|
||||||
particleCount,
|
// particleCount,
|
||||||
origin: {
|
// origin: {
|
||||||
x: randomInRange(0.7, 0.9),
|
// x: randomInRange(0.7, 0.9),
|
||||||
y: Math.random() - 0.2
|
// y: Math.random() - 0.2
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
}, 250);
|
// }, 250);
|
||||||
|
|
||||||
setPurchaseOptionsOpen(false);
|
// setPurchaseOptionsOpen(false);
|
||||||
setKeyOpen(false);
|
// setKeyOpen(false);
|
||||||
|
|
||||||
updateSupporterStatus({
|
// updateSupporterStatus({
|
||||||
visible: false
|
// visible: false
|
||||||
});
|
// });
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
toast({
|
// toast({
|
||||||
variant: "destructive",
|
// variant: "destructive",
|
||||||
title: t("error"),
|
// title: t("error"),
|
||||||
description: formatAxiosError(
|
// description: formatAxiosError(
|
||||||
error,
|
// error,
|
||||||
t("supportKeyErrorValidationDescription")
|
// t("supportKeyErrorValidationDescription")
|
||||||
)
|
// )
|
||||||
});
|
// });
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Credenza
|
{/* <Credenza
|
||||||
open={purchaseOptionsOpen}
|
open={purchaseOptionsOpen}
|
||||||
onOpenChange={(val) => {
|
onOpenChange={(val) => {
|
||||||
setPurchaseOptionsOpen(val);
|
setPurchaseOptionsOpen(val);
|
||||||
@@ -469,7 +469,7 @@ export default function SupporterStatus({
|
|||||||
{t("supportKeyBuy")}
|
{t("supportKeyBuy")}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null} */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { cache } from "react";
|
|||||||
|
|
||||||
export const getBrowserTargetForRequest = cache(async () => {
|
export const getBrowserTargetForRequest = cache(async () => {
|
||||||
const headersList = await headers();
|
const headersList = await headers();
|
||||||
const host = headersList.get("host") || "";
|
const host = headersList.get("p-host") || headersList.get("host") || "";
|
||||||
const hostname = host.split(":")[0];
|
const hostname = host.split(":")[0];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user