Compare commits
269 Commits
1.13.0-rc.
...
cicd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ed13b41d9 | ||
|
|
b80757a129 | ||
|
|
13ddf30781 | ||
|
|
4ecca88856 | ||
|
|
4f154d212e | ||
|
|
981d777a65 | ||
|
|
dd13758085 | ||
|
|
3d8153aeb1 | ||
|
|
9ffa391416 | ||
|
|
afc19f192b | ||
|
|
5587bd9d59 | ||
|
|
b5f8e8feb2 | ||
|
|
9bd66fa306 | ||
|
|
fea4d43920 | ||
|
|
d414617f9d | ||
|
|
1d7e55bf98 | ||
|
|
bc45e16109 | ||
|
|
4f1dc19569 | ||
|
|
1af938d7ea | ||
|
|
fc924f707c | ||
|
|
6e7ba1dc52 | ||
|
|
3e01bfef7d | ||
|
|
d8b662496b | ||
|
|
e0de003c2c | ||
|
|
6e35c182b0 | ||
|
|
2479a3c53c | ||
|
|
6b609bb078 | ||
|
|
9c21e3da16 | ||
|
|
7ccde11e3e | ||
|
|
56b0185c8f | ||
|
|
8b47b2aabe | ||
|
|
416fd914cb | ||
|
|
16653dd524 | ||
|
|
e2d3d172af | ||
|
|
137d6c2523 | ||
|
|
1a976c78ef | ||
|
|
e309a125f5 | ||
|
|
2bdb1ddb6f | ||
|
|
8ff588407c | ||
|
|
c2e06725a8 | ||
|
|
bb43e0c325 | ||
|
|
35ea01610a | ||
|
|
79eefc0ac7 | ||
|
|
3a781f9ac4 | ||
|
|
cc1e551f43 | ||
|
|
68191d5921 | ||
|
|
2b3d065650 | ||
|
|
7ae80d2cad | ||
|
|
acf08e3ef6 | ||
|
|
6f50fb8a4f | ||
|
|
a5b203af27 | ||
|
|
443b53ee37 | ||
|
|
e033c10021 | ||
|
|
ad4c44c325 | ||
|
|
4aef7ca8d5 | ||
|
|
f892acbc4c | ||
|
|
9010ed6237 | ||
|
|
9f29657570 | ||
|
|
1b13132845 | ||
|
|
553fda265c | ||
|
|
0f79826535 | ||
|
|
14438bd2b4 | ||
|
|
c4445c329f | ||
|
|
5c032ee0c3 | ||
|
|
d3d5a1c204 | ||
|
|
809bb4a7b4 | ||
|
|
e8f763a77f | ||
|
|
3ad4a76f03 | ||
|
|
b133593ea2 | ||
|
|
43fb06084f | ||
|
|
9de39dbe42 | ||
|
|
c98d61a8fb | ||
|
|
fccff9c23a | ||
|
|
e02fa7c148 | ||
|
|
a21029582e | ||
|
|
9ef7faace7 | ||
|
|
3d5ae9dd5c | ||
|
|
6072ee93fa | ||
|
|
7f7f6eeaea | ||
|
|
1b4884afd8 | ||
|
|
0c0ad7029f | ||
|
|
10f1437496 | ||
|
|
c44c1a5518 | ||
|
|
48110ccda3 | ||
|
|
e94f21bc05 | ||
|
|
65f8a414be | ||
|
|
8dad38775c | ||
|
|
0d14cb853e | ||
|
|
778e6bf623 | ||
|
|
5a960649db | ||
|
|
23a7688789 | ||
|
|
0e3b6b90b7 | ||
|
|
872bb557c2 | ||
|
|
9125a7bccb | ||
|
|
5a0a8893e8 | ||
|
|
abe76e5002 | ||
|
|
474b9a685d | ||
|
|
97631c068c | ||
|
|
98c77ad7e2 | ||
|
|
3915df3200 | ||
|
|
9b98acb553 | ||
|
|
a767a31c21 | ||
|
|
f2d4c2f83c | ||
|
|
25fed23758 | ||
|
|
5cb3fa1127 | ||
|
|
deac26bad2 | ||
|
|
c7747fd4b4 | ||
|
|
1aaad43871 | ||
|
|
143175bde7 | ||
|
|
9f55d6b20a | ||
|
|
4366ca5836 | ||
|
|
9cb95576d0 | ||
|
|
d5307adef0 | ||
|
|
3d857c3b52 | ||
|
|
a012369f83 | ||
|
|
9cee3d9c79 | ||
|
|
8257dca340 | ||
|
|
5e0a1cf9c5 | ||
|
|
b3ec9dfda2 | ||
|
|
93d4f60314 | ||
|
|
769d20cea1 | ||
|
|
124ba208de | ||
|
|
ba99614d58 | ||
|
|
27db77bca4 | ||
|
|
29b924230f | ||
|
|
8eb3f6aacc | ||
|
|
7f07ccea44 | ||
|
|
c13bfc709f | ||
|
|
6fc54bcc9e | ||
|
|
5d6ee45125 | ||
|
|
fceaedfcd8 | ||
|
|
181612ce25 | ||
|
|
224b78fc64 | ||
|
|
757e540be6 | ||
|
|
bf1675686c | ||
|
|
f81909489a | ||
|
|
963468d7fa | ||
|
|
f67f4f8834 | ||
|
|
4c819d264b | ||
|
|
cbcb23ccea | ||
|
|
d8b27de5ac | ||
|
|
01f7842fd5 | ||
|
|
d409e58186 | ||
|
|
c9e1c4da1c | ||
|
|
9c38f65ad4 | ||
|
|
2316462721 | ||
|
|
7cc990107a | ||
|
|
9917a569ac | ||
|
|
aab0471b6b | ||
|
|
de684b212f | ||
|
|
fbd3802e46 | ||
|
|
4e842a660a | ||
|
|
ce6b609ca2 | ||
|
|
78369b6f6a | ||
|
|
ea43bf97c7 | ||
|
|
c56574e431 | ||
|
|
f9c0e0ec3d | ||
|
|
85986dcccb | ||
|
|
c9779254c3 | ||
|
|
5b620469c7 | ||
|
|
df4b9de334 | ||
|
|
d490cab48c | ||
|
|
b68c0962c6 | ||
|
|
ee2a438602 | ||
|
|
74dd3fdc9f | ||
|
|
314da3ee3e | ||
|
|
68cfc84249 | ||
|
|
0bcf5c2b42 | ||
|
|
9210e005e9 | ||
|
|
f245632371 | ||
|
|
6453b070bb | ||
|
|
8c4db93a93 | ||
|
|
f9b03943c3 | ||
|
|
fa839a811f | ||
|
|
88d2c2eac8 | ||
|
|
c84cc1815b | ||
|
|
2c23ffd178 | ||
|
|
da3f7ae404 | ||
|
|
f460559a4b | ||
|
|
0c9deeb2d7 | ||
|
|
1289b99f14 | ||
|
|
1a7a6e5b6f | ||
|
|
f56135eed3 | ||
|
|
23e9a61f3e | ||
|
|
5428ad1009 | ||
|
|
bba28bc5f2 | ||
|
|
18498a32ce | ||
|
|
887af85db1 | ||
|
|
a306aa971b | ||
|
|
0a9b19ecfc | ||
|
|
e011580b96 | ||
|
|
048ce850a8 | ||
|
|
2ca1f15add | ||
|
|
05ebd547b5 | ||
|
|
5a8b1383a4 | ||
|
|
ede51bebb5 | ||
|
|
fd29071d57 | ||
|
|
8e1af79dc4 | ||
|
|
dc8c28626d | ||
|
|
9db2feff77 | ||
|
|
adf76bfb53 | ||
|
|
e0a79b7d4d | ||
|
|
9ea3914a93 | ||
|
|
1aeb31be04 | ||
|
|
64120ea878 | ||
|
|
0003ec021b | ||
|
|
c9a1da210f | ||
|
|
ace402af2d | ||
|
|
e60dce25c9 | ||
|
|
ccfff030e5 | ||
|
|
00765c1faf | ||
|
|
f6bbdeadb9 | ||
|
|
9cf520574a | ||
|
|
f8ab5b7af7 | ||
|
|
72bc26f0f8 | ||
|
|
2ec2295cd6 | ||
|
|
a0a369dc43 | ||
|
|
d0157ea7a5 | ||
|
|
d89f5279bf | ||
|
|
744305ab39 | ||
|
|
ba9048a377 | ||
|
|
ff089ec6d7 | ||
|
|
dc4f9a9bd1 | ||
|
|
e867de023a | ||
|
|
e00c3f2193 | ||
|
|
8c30995228 | ||
|
|
3ba65a3311 | ||
|
|
c5914dc0c0 | ||
|
|
30f3ab11b2 | ||
|
|
66b01b764f | ||
|
|
ee7e7778b6 | ||
|
|
0d0c43f72b | ||
|
|
83f36bce9d | ||
|
|
2466d24c1a | ||
|
|
2f34def4d7 | ||
|
|
8e8f992876 | ||
|
|
1d9ed9d219 | ||
|
|
616fb9c8e9 | ||
|
|
a2ab7191e5 | ||
|
|
7a31292ec7 | ||
|
|
196fbbe334 | ||
|
|
5bb5aeff36 | ||
|
|
2ada05b286 | ||
|
|
87f23f582c | ||
|
|
29a52f6ac4 | ||
|
|
790f7083e2 | ||
|
|
5c851e82ff | ||
|
|
854f638da3 | ||
|
|
4842648e7b | ||
|
|
8f152bdf9f | ||
|
|
d003436179 | ||
|
|
9776ef43ea | ||
|
|
e2c4a906c4 | ||
|
|
27e8250cd1 | ||
|
|
0d84b7af6e | ||
|
|
b961271aa6 | ||
|
|
b505cc60b0 | ||
|
|
955f927c59 | ||
|
|
4beed9d464 | ||
|
|
228481444f | ||
|
|
02cd2cfb17 | ||
|
|
d218a4bbc3 | ||
|
|
4bd1c4e0c6 | ||
|
|
cfde4e7443 | ||
|
|
f58cf68f7c | ||
|
|
08e43400e4 | ||
|
|
46d60bd090 | ||
|
|
5641a2aa31 | ||
|
|
0abc561bb8 |
@@ -1,6 +1,3 @@
|
||||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"next/typescript"
|
||||
]
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
|
||||
190
.github/workflows/cicd.yml
vendored
@@ -24,9 +24,35 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and Release
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
pre-run:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Start EC2 instances
|
||||
run: |
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||
echo "EC2 instances started"
|
||||
|
||||
|
||||
release-arm:
|
||||
name: Build and Release (ARM64)
|
||||
runs-on: [self-hosted, linux, arm64, us-east-1]
|
||||
needs: [pre-run]
|
||||
if: >-
|
||||
${{
|
||||
needs.pre-run.result == 'success'
|
||||
}}
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
@@ -36,13 +62,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
THRESHOLD=75
|
||||
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||
echo "Used space: $USED_SPACE%"
|
||||
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||
echo y | docker system prune -a
|
||||
else
|
||||
echo "Storage space is above the threshold. No action needed."
|
||||
fi
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
@@ -50,6 +82,103 @@ jobs:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||
cat server/lib/consts.ts
|
||||
shell: bash
|
||||
|
||||
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
make build-release-arm tag=$TAG
|
||||
echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
release-amd:
|
||||
name: Build and Release (AMD64)
|
||||
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||
needs: [pre-run]
|
||||
if: >-
|
||||
${{
|
||||
needs.pre-run.result == 'success'
|
||||
}}
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
# Target images
|
||||
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
|
||||
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Monitor storage space
|
||||
run: |
|
||||
THRESHOLD=75
|
||||
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||
echo "Used space: $USED_SPACE%"
|
||||
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||
echo y | docker system prune -a
|
||||
else
|
||||
echo "Storage space is above the threshold. No action needed."
|
||||
fi
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Update version in package.json
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||
cat server/lib/consts.ts
|
||||
shell: bash
|
||||
|
||||
- name: Build and push Docker images (Docker Hub - AMD64)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
make build-release-amd tag=$TAG
|
||||
echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
sign-and-package:
|
||||
name: Sign and Package
|
||||
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||
needs: [release-arm, release-amd]
|
||||
if: >-
|
||||
${{
|
||||
needs.release-arm.result == 'success' &&
|
||||
needs.release-amd.result == 'success'
|
||||
}}
|
||||
# Job-level timeout to avoid runaway or stuck runs
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
# Target images
|
||||
DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
|
||||
GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Extract tag name
|
||||
id: get-tag
|
||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
@@ -96,7 +225,7 @@ jobs:
|
||||
- name: Build installer
|
||||
working-directory: install
|
||||
run: |
|
||||
make go-build-release
|
||||
make go-build-release
|
||||
|
||||
- name: Upload artifacts from /install/bin
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
@@ -104,13 +233,6 @@ jobs:
|
||||
name: install-bin
|
||||
path: install/bin/
|
||||
|
||||
- name: Build and push Docker images (Docker Hub)
|
||||
run: |
|
||||
TAG=${{ env.TAG }}
|
||||
make build-release tag=$TAG
|
||||
echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||
shell: bash
|
||||
|
||||
- name: Install skopeo + jq
|
||||
# skopeo: copy/inspect images between registries
|
||||
# jq: JSON parsing tool used to extract digest values
|
||||
@@ -127,15 +249,18 @@ jobs:
|
||||
|
||||
- name: Copy tag from Docker Hub to GHCR
|
||||
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
||||
# Wait a bit for both architectures to be available in Docker Hub manifest
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG=${{ env.TAG }}
|
||||
echo "Waiting for multi-arch manifest to be ready..."
|
||||
sleep 30
|
||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||
skopeo copy --all --retry-times 3 \
|
||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||
docker://$GHCR_IMAGE:$TAG
|
||||
shell: bash
|
||||
|
||||
|
||||
- name: Login to GitHub Container Registry (for cosign)
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
@@ -185,3 +310,32 @@ jobs:
|
||||
"${REF}" -o text
|
||||
done
|
||||
shell: bash
|
||||
|
||||
post-run:
|
||||
needs: [pre-run, release-arm, release-amd, sign-and-package]
|
||||
if: >-
|
||||
${{
|
||||
always() &&
|
||||
needs.pre-run.result == 'success' &&
|
||||
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
|
||||
(needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
|
||||
(needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
|
||||
}}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Stop EC2 instances
|
||||
run: |
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||
echo "EC2 instances stopped"
|
||||
|
||||
4
.github/workflows/linting.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
|
||||
39
.github/workflows/restart-runners.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Restart Runners
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 */7 * *'
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ec2-maintenance-prod:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
|
||||
- name: Verify AWS identity
|
||||
run: aws sts get-caller-identity
|
||||
|
||||
- name: Start EC2 instance
|
||||
run: |
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||
echo "EC2 instances started"
|
||||
|
||||
- name: Wait
|
||||
run: sleep 600
|
||||
|
||||
- name: Stop EC2 instance
|
||||
run: |
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||
echo "EC2 instances stopped"
|
||||
2
.github/workflows/stale-bot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
days-before-stale: 14
|
||||
days-before-close: 14
|
||||
|
||||
29
.github/workflows/test.yml
vendored
@@ -12,11 +12,12 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
@@ -57,8 +58,26 @@ jobs:
|
||||
echo "App failed to start"
|
||||
exit 1
|
||||
|
||||
build-sqlite:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Copy config file
|
||||
run: cp config/config.example.yml config/config.yml
|
||||
|
||||
- name: Build Docker image sqlite
|
||||
run: make build-sqlite
|
||||
run: make dev-build-sqlite
|
||||
|
||||
build-postgres:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Copy config file
|
||||
run: cp config/config.example.yml config/config.yml
|
||||
|
||||
- name: Build Docker image pg
|
||||
run: make build-pg
|
||||
run: make dev-build-pg
|
||||
|
||||
12
.prettierignore
Normal file
@@ -0,0 +1,12 @@
|
||||
.github/
|
||||
bruno/
|
||||
cli/
|
||||
config/
|
||||
messages/
|
||||
next.config.mjs/
|
||||
public/
|
||||
tailwind.config.js/
|
||||
test/
|
||||
**/*.yml
|
||||
**/*.yaml
|
||||
**/*.md
|
||||
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
22
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.addMissingImports.ts": "always"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
16
Dockerfile
@@ -43,23 +43,25 @@ RUN test -f dist/server.mjs
|
||||
|
||||
RUN npm run build:cli
|
||||
|
||||
# Prune dev dependencies and clean up to prepare for copy to runner
|
||||
RUN npm prune --omit=dev && npm cache clean --force
|
||||
|
||||
FROM node:24-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Curl used for the health checks
|
||||
# Python and build tools needed for better-sqlite3 native compilation
|
||||
RUN apk add --no-cache curl tzdata python3 make g++
|
||||
# Only curl and tzdata needed at runtime - no build tools!
|
||||
RUN apk add --no-cache curl tzdata
|
||||
|
||||
# COPY package.json package-lock.json ./
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
# Copy pre-built node_modules from builder (already pruned to production only)
|
||||
# This includes the compiled native modules like better-sqlite3
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/init ./dist/init
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||
|
||||
119
Makefile
@@ -1,8 +1,13 @@
|
||||
.PHONY: build build-pg build-release build-arm build-x86 test clean
|
||||
.PHONY: build build-pg build-release build-release-arm build-release-amd build-arm build-x86 test clean
|
||||
|
||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
||||
build-release:
|
||||
|
||||
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||
|
||||
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||
|
||||
build-sqlite:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
@@ -16,6 +21,12 @@ build-release:
|
||||
--tag fosrl/pangolin:$(minor_tag) \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push .
|
||||
|
||||
build-postgresql:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
@@ -25,6 +36,12 @@ build-release:
|
||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-ee-sqlite:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
@@ -34,6 +51,12 @@ build-release:
|
||||
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push .
|
||||
|
||||
build-ee-postgresql:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
@@ -44,6 +67,94 @@ build-release:
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-release-arm:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:latest \
|
||||
--tag fosrl/pangolin:$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:postgresql-latest \
|
||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:postgresql-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-latest \
|
||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:ee-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/arm64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-release-amd:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release-amd tag=<tag>"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:latest \
|
||||
--tag fosrl/pangolin:$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:$(tag) \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=oss \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:postgresql-latest \
|
||||
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:postgresql-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=sqlite \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-latest \
|
||||
--tag fosrl/pangolin:ee-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:ee-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:ee-$(tag) \
|
||||
--push . && \
|
||||
docker buildx build \
|
||||
--build-arg BUILD=enterprise \
|
||||
--build-arg DATABASE=pg \
|
||||
--platform linux/amd64 \
|
||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG \
|
||||
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG \
|
||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||
--push .
|
||||
|
||||
build-rc:
|
||||
@if [ -z "$(tag)" ]; then \
|
||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||
@@ -80,10 +191,10 @@ build-arm:
|
||||
build-x86:
|
||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||
|
||||
build-sqlite:
|
||||
dev-build-sqlite:
|
||||
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
||||
|
||||
build-pg:
|
||||
dev-build-pg:
|
||||
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
||||
|
||||
test:
|
||||
|
||||
22
README.md
@@ -31,7 +31,7 @@
|
||||
[](https://pangolin.net/slack)
|
||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||

|
||||
[](https://www.youtube.com/@fossorial-app)
|
||||
[](https://www.youtube.com/@pangolin-net)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN.
|
||||
Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources, all with zero-trust security and granular access control.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -60,14 +60,20 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and contex
|
||||
|
||||
## Key Features
|
||||
|
||||
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
|
||||
|
||||
| <img width=500 /> | <img width=500 /> |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
|
||||
| **Manage applications in one place**<br /><br /> Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. | <img src="public/screenshots/hero.png" width=500 /><tr></tr> |
|
||||
| **Reverse proxy across networks anywhere**<br /><br />Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||
| **Enforce identity and context aware rules**<br /><br />Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. | <img src="public/auth-diagram1.png" width=500 /><tr></tr> |
|
||||
| **Quickly connect Pangolin sites**<br /><br />Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||
| **Connect remote networks with sites**<br /><br />Pangolin's lightweight site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | <img src="public/screenshots/sites.png" width=500 /><tr></tr> |
|
||||
| **Browser-based reverse proxy access**<br /><br />Expose web applications through identity and context-aware tunneled reverse proxies. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. Users access applications through any web browser with authentication and granular access control. | <img src="public/clip.gif" width=500 /><tr></tr> |
|
||||
| **Client-based private resource access**<br /><br />Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. | <img src="public/screenshots/private-resources.png" width=500 /><tr></tr> |
|
||||
| **Zero-trust granular access**<br /><br />Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications and services you explicitly define, reducing security risk and attack surface. | <img src="public/screenshots/user-devices.png" width=500 /><tr></tr> |
|
||||
|
||||
## Download Clients
|
||||
|
||||
Download the Pangolin client for your platform:
|
||||
|
||||
- [Mac](https://pangolin.net/downloads/mac)
|
||||
- [Windows](https://pangolin.net/downloads/windows)
|
||||
- [Linux](https://pangolin.net/downloads/linux)
|
||||
|
||||
## Get Started
|
||||
|
||||
|
||||
36
cli/commands/clearExitNodes.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
type ClearExitNodesArgs = { };
|
||||
|
||||
export const clearExitNodes: CommandModule<
|
||||
{},
|
||||
ClearExitNodesArgs
|
||||
> = {
|
||||
command: "clear-exit-nodes",
|
||||
describe:
|
||||
"Clear all exit nodes from the database",
|
||||
// no args
|
||||
builder: (yargs) => {
|
||||
return yargs;
|
||||
},
|
||||
handler: async (argv: {}) => {
|
||||
try {
|
||||
|
||||
console.log(`Clearing all exit nodes from the database`);
|
||||
|
||||
// Delete all exit nodes
|
||||
const deletedCount = await db
|
||||
.delete(exitNodes)
|
||||
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)) .returning();; // delete all
|
||||
|
||||
console.log(`Deleted ${deletedCount.length} exit node(s) from the database`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
284
cli/commands/rotateServerSecret.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, idpOidcConfig, licenseKey } from "@server/db";
|
||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||
import { eq } from "drizzle-orm";
|
||||
import fs from "fs";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
type RotateServerSecretArgs = {
|
||||
oldSecret: string;
|
||||
newSecret: string;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export const rotateServerSecret: CommandModule<
|
||||
{},
|
||||
RotateServerSecretArgs
|
||||
> = {
|
||||
command: "rotate-server-secret",
|
||||
describe:
|
||||
"Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret",
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.option("oldSecret", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The current server secret (for verification)"
|
||||
})
|
||||
.option("newSecret", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The new server secret to use"
|
||||
})
|
||||
.option("force", {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
describe:
|
||||
"Force rotation even if the old secret doesn't match the config file. " +
|
||||
"Use this if you know the old secret is correct but the config file is out of sync. " +
|
||||
"WARNING: This will attempt to decrypt all values with the provided old secret. " +
|
||||
"If the old secret is incorrect, the rotation will fail or corrupt data."
|
||||
});
|
||||
},
|
||||
handler: async (argv: {
|
||||
oldSecret: string;
|
||||
newSecret: string;
|
||||
force?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
// Determine which config file exists
|
||||
const configPath = fs.existsSync(configFilePath1)
|
||||
? configFilePath1
|
||||
: fs.existsSync(configFilePath2)
|
||||
? configFilePath2
|
||||
: null;
|
||||
|
||||
if (!configPath) {
|
||||
console.error(
|
||||
"Error: Config file not found. Expected config.yml or config.yaml in the config directory."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Read current config
|
||||
const configContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(configContent) as any;
|
||||
|
||||
if (!config?.server?.secret) {
|
||||
console.error(
|
||||
"Error: No server secret found in config file. Cannot rotate."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configSecret = config.server.secret;
|
||||
const oldSecret = argv.oldSecret;
|
||||
const newSecret = argv.newSecret;
|
||||
const force = argv.force || false;
|
||||
|
||||
// Verify that the provided old secret matches the one in config
|
||||
if (configSecret !== oldSecret) {
|
||||
if (!force) {
|
||||
console.error(
|
||||
"Error: The provided old secret does not match the secret in the config file."
|
||||
);
|
||||
console.error(
|
||||
"\nIf you are certain the old secret is correct and the config file is out of sync,"
|
||||
);
|
||||
console.error(
|
||||
"you can use the --force flag to bypass this check."
|
||||
);
|
||||
console.error(
|
||||
"\nWARNING: Using --force with an incorrect old secret will cause the rotation to fail"
|
||||
);
|
||||
console.error(
|
||||
"or corrupt encrypted data. Only use --force if you are absolutely certain."
|
||||
);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.warn(
|
||||
"\nWARNING: Using --force flag. Bypassing old secret verification."
|
||||
);
|
||||
console.warn(
|
||||
"The provided old secret does not match the config file, but proceeding anyway."
|
||||
);
|
||||
console.warn(
|
||||
"If the old secret is incorrect, this operation will fail or corrupt data.\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate new secret
|
||||
if (newSecret.length < 8) {
|
||||
console.error(
|
||||
"Error: New secret must be at least 8 characters long"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (oldSecret === newSecret) {
|
||||
console.error("Error: New secret must be different from old secret");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Starting server secret rotation...");
|
||||
console.log("This will decrypt and re-encrypt all encrypted values in the database.");
|
||||
|
||||
// Read all data first
|
||||
console.log("\nReading encrypted data from database...");
|
||||
const idpConfigs = await db.select().from(idpOidcConfig);
|
||||
const licenseKeys = await db.select().from(licenseKey);
|
||||
|
||||
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
|
||||
console.log(`Found ${licenseKeys.length} license key(s)`);
|
||||
|
||||
// Prepare all decrypted and re-encrypted values
|
||||
console.log("\nDecrypting and re-encrypting values...");
|
||||
|
||||
type IdpUpdate = {
|
||||
idpOauthConfigId: number;
|
||||
encryptedClientId: string;
|
||||
encryptedClientSecret: string;
|
||||
};
|
||||
|
||||
type LicenseKeyUpdate = {
|
||||
oldLicenseKeyId: string;
|
||||
newLicenseKeyId: string;
|
||||
encryptedToken: string;
|
||||
encryptedInstanceId: string;
|
||||
};
|
||||
|
||||
const idpUpdates: IdpUpdate[] = [];
|
||||
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
||||
|
||||
// Process idpOidcConfig entries
|
||||
for (const idpConfig of idpConfigs) {
|
||||
try {
|
||||
// Decrypt with old secret
|
||||
const decryptedClientId = decrypt(idpConfig.clientId, oldSecret);
|
||||
const decryptedClientSecret = decrypt(
|
||||
idpConfig.clientSecret,
|
||||
oldSecret
|
||||
);
|
||||
|
||||
// Re-encrypt with new secret
|
||||
const encryptedClientId = encrypt(decryptedClientId, newSecret);
|
||||
const encryptedClientSecret = encrypt(
|
||||
decryptedClientSecret,
|
||||
newSecret
|
||||
);
|
||||
|
||||
idpUpdates.push({
|
||||
idpOauthConfigId: idpConfig.idpOauthConfigId,
|
||||
encryptedClientId,
|
||||
encryptedClientSecret
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing IdP config ${idpConfig.idpOauthConfigId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Process licenseKey entries
|
||||
for (const key of licenseKeys) {
|
||||
try {
|
||||
// Decrypt with old secret
|
||||
const decryptedLicenseKeyId = decrypt(key.licenseKeyId, oldSecret);
|
||||
const decryptedToken = decrypt(key.token, oldSecret);
|
||||
const decryptedInstanceId = decrypt(key.instanceId, oldSecret);
|
||||
|
||||
// Re-encrypt with new secret
|
||||
const encryptedLicenseKeyId = encrypt(
|
||||
decryptedLicenseKeyId,
|
||||
newSecret
|
||||
);
|
||||
const encryptedToken = encrypt(decryptedToken, newSecret);
|
||||
const encryptedInstanceId = encrypt(
|
||||
decryptedInstanceId,
|
||||
newSecret
|
||||
);
|
||||
|
||||
licenseKeyUpdates.push({
|
||||
oldLicenseKeyId: key.licenseKeyId,
|
||||
newLicenseKeyId: encryptedLicenseKeyId,
|
||||
encryptedToken,
|
||||
encryptedInstanceId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error processing license key ${key.licenseKeyId}:`,
|
||||
error
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform all database updates in a single transaction
|
||||
console.log("\nUpdating database in transaction...");
|
||||
await db.transaction(async (trx) => {
|
||||
// Update idpOidcConfig entries
|
||||
for (const update of idpUpdates) {
|
||||
await trx
|
||||
.update(idpOidcConfig)
|
||||
.set({
|
||||
clientId: update.encryptedClientId,
|
||||
clientSecret: update.encryptedClientSecret
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
idpOidcConfig.idpOauthConfigId,
|
||||
update.idpOauthConfigId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update licenseKey entries (delete old, insert new)
|
||||
for (const update of licenseKeyUpdates) {
|
||||
// Delete old entry
|
||||
await trx
|
||||
.delete(licenseKey)
|
||||
.where(eq(licenseKey.licenseKeyId, update.oldLicenseKeyId));
|
||||
|
||||
// Insert new entry with re-encrypted values
|
||||
await trx.insert(licenseKey).values({
|
||||
licenseKeyId: update.newLicenseKeyId,
|
||||
token: update.encryptedToken,
|
||||
instanceId: update.encryptedInstanceId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
|
||||
console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
|
||||
|
||||
// Update config file with new secret
|
||||
console.log("\nUpdating config file...");
|
||||
config.server.secret = newSecret;
|
||||
const newConfigContent = yaml.dump(config, {
|
||||
indent: 2,
|
||||
lineWidth: -1
|
||||
});
|
||||
fs.writeFileSync(configPath, newConfigContent, "utf8");
|
||||
|
||||
console.log(`Updated config file: ${configPath}`);
|
||||
|
||||
console.log("\nServer secret rotation completed successfully!");
|
||||
console.log(`\nSummary:`);
|
||||
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
|
||||
console.log(` - License keys: ${licenseKeyUpdates.length}`);
|
||||
console.log(
|
||||
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error rotating server secret:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,10 +4,14 @@ import yargs from "yargs";
|
||||
import { hideBin } from "yargs/helpers";
|
||||
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
||||
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
||||
import { clearExitNodes } from "./commands/clearExitNodes";
|
||||
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName("pangctl")
|
||||
.command(setAdminCredentials)
|
||||
.command(resetUserSecurityKeys)
|
||||
.command(clearExitNodes)
|
||||
.command(rotateServerSecret)
|
||||
.demandCommand()
|
||||
.help().argv;
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
const schema = [
|
||||
path.join("server", "db", "pg", "schema"),
|
||||
];
|
||||
const schema = [path.join("server", "db", "pg", "schema")];
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
|
||||
@@ -2,9 +2,7 @@ import { APP_PATH } from "@server/lib/consts";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
import path from "path";
|
||||
|
||||
const schema = [
|
||||
path.join("server", "db", "sqlite", "schema"),
|
||||
];
|
||||
const schema = [path.join("server", "db", "sqlite", "schema")];
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
|
||||
95
esbuild.mjs
@@ -24,20 +24,20 @@ const argv = yargs(hideBin(process.argv))
|
||||
alias: "e",
|
||||
describe: "Entry point file",
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
demandOption: true
|
||||
})
|
||||
.option("out", {
|
||||
alias: "o",
|
||||
describe: "Output file path",
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
demandOption: true
|
||||
})
|
||||
.option("build", {
|
||||
alias: "b",
|
||||
describe: "Build type (oss, saas, enterprise)",
|
||||
type: "string",
|
||||
choices: ["oss", "saas", "enterprise"],
|
||||
default: "oss",
|
||||
default: "oss"
|
||||
})
|
||||
.help()
|
||||
.alias("help", "h").argv;
|
||||
@@ -66,7 +66,9 @@ function privateImportGuardPlugin() {
|
||||
|
||||
// Check if the importing file is NOT in server/private
|
||||
const normalizedImporter = path.normalize(importingFile);
|
||||
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||
const isInServerPrivate = normalizedImporter.includes(
|
||||
path.normalize("server/private")
|
||||
);
|
||||
|
||||
if (!isInServerPrivate) {
|
||||
const violation = {
|
||||
@@ -79,8 +81,8 @@ function privateImportGuardPlugin() {
|
||||
console.log(`PRIVATE IMPORT VIOLATION:`);
|
||||
console.log(` File: ${importingFile}`);
|
||||
console.log(` Import: ${args.path}`);
|
||||
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||
console.log('');
|
||||
console.log(` Resolve dir: ${args.resolveDir || "N/A"}`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
// Return null to let the default resolver handle it
|
||||
@@ -89,16 +91,20 @@ function privateImportGuardPlugin() {
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (violations.length > 0) {
|
||||
console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
|
||||
console.log(
|
||||
`\nSUMMARY: Found ${violations.length} private import violation(s):`
|
||||
);
|
||||
violations.forEach((v, i) => {
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||
console.log(
|
||||
` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`
|
||||
);
|
||||
});
|
||||
console.log('');
|
||||
console.log("");
|
||||
|
||||
result.errors.push({
|
||||
text: `Private import violations detected: ${violations.length} violation(s) found`,
|
||||
location: null,
|
||||
notes: violations.map(v => ({
|
||||
notes: violations.map((v) => ({
|
||||
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||
location: null
|
||||
}))
|
||||
@@ -121,7 +127,9 @@ function dynamicImportGuardPlugin() {
|
||||
|
||||
// Check if the importing file is NOT in server/private
|
||||
const normalizedImporter = path.normalize(importingFile);
|
||||
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
||||
const isInServerPrivate = normalizedImporter.includes(
|
||||
path.normalize("server/private")
|
||||
);
|
||||
|
||||
if (isInServerPrivate) {
|
||||
const violation = {
|
||||
@@ -134,8 +142,8 @@ function dynamicImportGuardPlugin() {
|
||||
console.log(`DYNAMIC IMPORT VIOLATION:`);
|
||||
console.log(` File: ${importingFile}`);
|
||||
console.log(` Import: ${args.path}`);
|
||||
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
||||
console.log('');
|
||||
console.log(` Resolve dir: ${args.resolveDir || "N/A"}`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
// Return null to let the default resolver handle it
|
||||
@@ -144,16 +152,20 @@ function dynamicImportGuardPlugin() {
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (violations.length > 0) {
|
||||
console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
|
||||
console.log(
|
||||
`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`
|
||||
);
|
||||
violations.forEach((v, i) => {
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
||||
console.log(
|
||||
` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`
|
||||
);
|
||||
});
|
||||
console.log('');
|
||||
console.log("");
|
||||
|
||||
result.errors.push({
|
||||
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
|
||||
location: null,
|
||||
notes: violations.map(v => ({
|
||||
notes: violations.map((v) => ({
|
||||
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||
location: null
|
||||
}))
|
||||
@@ -172,21 +184,28 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
||||
const switches = [];
|
||||
|
||||
build.onStart(() => {
|
||||
console.log(`Dynamic import switcher using build type: ${buildValue}`);
|
||||
console.log(
|
||||
`Dynamic import switcher using build type: ${buildValue}`
|
||||
);
|
||||
});
|
||||
|
||||
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||
// Extract the path after #dynamic/
|
||||
const dynamicPath = args.path.replace(/^#dynamic\//, '');
|
||||
const dynamicPath = args.path.replace(/^#dynamic\//, "");
|
||||
|
||||
// Determine the replacement based on build type
|
||||
let replacement;
|
||||
if (buildValue === "oss") {
|
||||
replacement = `#open/${dynamicPath}`;
|
||||
} else if (buildValue === "saas" || buildValue === "enterprise") {
|
||||
} else if (
|
||||
buildValue === "saas" ||
|
||||
buildValue === "enterprise"
|
||||
) {
|
||||
replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private
|
||||
} else {
|
||||
console.warn(`Unknown build type '${buildValue}', defaulting to #open/`);
|
||||
console.warn(
|
||||
`Unknown build type '${buildValue}', defaulting to #open/`
|
||||
);
|
||||
replacement = `#open/${dynamicPath}`;
|
||||
}
|
||||
|
||||
@@ -201,8 +220,10 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
||||
console.log(`DYNAMIC IMPORT SWITCH:`);
|
||||
console.log(` File: ${args.importer}`);
|
||||
console.log(` Original: ${args.path}`);
|
||||
console.log(` Switched to: ${replacement} (build: ${buildValue})`);
|
||||
console.log('');
|
||||
console.log(
|
||||
` Switched to: ${replacement} (build: ${buildValue})`
|
||||
);
|
||||
console.log("");
|
||||
|
||||
// Rewrite the import path and let the normal resolution continue
|
||||
return build.resolve(replacement, {
|
||||
@@ -215,12 +236,18 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
||||
|
||||
build.onEnd((result) => {
|
||||
if (switches.length > 0) {
|
||||
console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
|
||||
console.log(
|
||||
`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`
|
||||
);
|
||||
switches.forEach((s, i) => {
|
||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`);
|
||||
console.log(` ${s.originalPath} → ${s.replacementPath}`);
|
||||
console.log(
|
||||
` ${i + 1}. ${path.relative(process.cwd(), s.file)}`
|
||||
);
|
||||
console.log(
|
||||
` ${s.originalPath} → ${s.replacementPath}`
|
||||
);
|
||||
});
|
||||
console.log('');
|
||||
console.log("");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -235,7 +262,7 @@ esbuild
|
||||
format: "esm",
|
||||
minify: false,
|
||||
banner: {
|
||||
js: banner,
|
||||
js: banner
|
||||
},
|
||||
platform: "node",
|
||||
external: ["body-parser"],
|
||||
@@ -244,20 +271,22 @@ esbuild
|
||||
dynamicImportGuardPlugin(),
|
||||
dynamicImportSwitcherPlugin(argv.build),
|
||||
nodeExternalsPlugin({
|
||||
packagePath: getPackagePaths(),
|
||||
}),
|
||||
packagePath: getPackagePaths()
|
||||
})
|
||||
],
|
||||
sourcemap: "inline",
|
||||
target: "node22",
|
||||
target: "node22"
|
||||
})
|
||||
.then((result) => {
|
||||
// Check if there were any errors in the build result
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
console.error(`Build failed with ${result.errors.length} error(s):`);
|
||||
console.error(
|
||||
`Build failed with ${result.errors.length} error(s):`
|
||||
);
|
||||
result.errors.forEach((error, i) => {
|
||||
console.error(`${i + 1}. ${error.text}`);
|
||||
if (error.notes) {
|
||||
error.notes.forEach(note => {
|
||||
error.notes.forEach((note) => {
|
||||
console.error(` - ${note.text}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config({
|
||||
files: ["**/*.{ts,tsx,js,jsx}"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
files: ["**/*.{ts,tsx,js,jsx}"],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
semi: "error",
|
||||
"prefer-const": "warn"
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"semi": "error",
|
||||
"prefer-const": "warn"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,10 +9,15 @@ services:
|
||||
PARSERS: crowdsecurity/whitelists
|
||||
ENROLL_TAGS: docker
|
||||
healthcheck:
|
||||
interval: 10s
|
||||
retries: 15
|
||||
timeout: 10s
|
||||
test: ["CMD", "cscli", "capi", "status"]
|
||||
test:
|
||||
- CMD
|
||||
- cscli
|
||||
- lapi
|
||||
- status
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
labels:
|
||||
- "traefik.enable=false" # Disable traefik for crowdsec
|
||||
volumes:
|
||||
|
||||
@@ -44,7 +44,7 @@ http:
|
||||
crowdsecAppsecUnreachableBlock: true # Block on unreachable
|
||||
crowdsecAppsecBodyLimit: 10485760
|
||||
crowdsecLapiKey: "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK" # CrowdSec API key which you noted down later
|
||||
crowdsecLapiHost: crowdsec:8080 # CrowdSec
|
||||
crowdsecLapiHost: crowdsec:8080 # CrowdSec
|
||||
crowdsecLapiScheme: http # CrowdSec API scheme
|
||||
forwardedHeadersTrustedIPs: # Forwarded headers trusted IPs
|
||||
- "0.0.0.0/0" # All IP addresses are trusted for forwarded headers (CHANGE MADE HERE)
|
||||
@@ -106,4 +106,13 @@ http:
|
||||
api-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
|
||||
@@ -73,7 +73,7 @@ func installDocker() error {
|
||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl gpg &&
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
@@ -82,7 +82,7 @@ func installDocker() error {
|
||||
case strings.Contains(osRelease, "ID=debian"):
|
||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||
apt-get update &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
||||
apt-get install -y apt-transport-https ca-certificates curl gpg &&
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||
apt-get update &&
|
||||
|
||||
@@ -3,8 +3,8 @@ module installer
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
golang.org/x/term v0.37.0
|
||||
golang.org/x/term v0.38.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.38.0 // indirect
|
||||
require golang.org/x/sys v0.39.0 // indirect
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -1043,7 +1043,7 @@
|
||||
"actionDeleteSite": "Standort löschen",
|
||||
"actionGetSite": "Standort abrufen",
|
||||
"actionListSites": "Standorte auflisten",
|
||||
"actionApplyBlueprint": "Blaupause anwenden",
|
||||
"actionApplyBlueprint": "Blueprint anwenden",
|
||||
"setupToken": "Setup-Token",
|
||||
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
|
||||
"setupTokenRequired": "Setup-Token ist erforderlich",
|
||||
@@ -1102,7 +1102,7 @@
|
||||
"actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen",
|
||||
"actionListIdpOrgs": "IDP-Organisationen auflisten",
|
||||
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
|
||||
"actionCreateClient": "Endgerät anlegen",
|
||||
"actionCreateClient": "Client erstellen",
|
||||
"actionDeleteClient": "Client löschen",
|
||||
"actionUpdateClient": "Client aktualisieren",
|
||||
"actionListClients": "Clients auflisten",
|
||||
@@ -1201,24 +1201,24 @@
|
||||
"sidebarLogsAnalytics": "Analytik",
|
||||
"blueprints": "Baupläne",
|
||||
"blueprintsDescription": "Deklarative Konfigurationen anwenden und vorherige Abläufe anzeigen",
|
||||
"blueprintAdd": "Blaupause hinzufügen",
|
||||
"blueprintGoBack": "Alle Blaupausen ansehen",
|
||||
"blueprintCreate": "Blaupause erstellen",
|
||||
"blueprintCreateDescription2": "Folge den Schritten unten, um eine neue Blaupause zu erstellen und anzuwenden",
|
||||
"blueprintDetails": "Blaupausendetails",
|
||||
"blueprintDetailsDescription": "Siehe das Ergebnis der angewendeten Blaupause und alle aufgetretenen Fehler",
|
||||
"blueprintInfo": "Blaupauseninformation",
|
||||
"blueprintAdd": "Blueprint hinzufügen",
|
||||
"blueprintGoBack": "Alle Blueprints ansehen",
|
||||
"blueprintCreate": "Blueprint erstellen",
|
||||
"blueprintCreateDescription2": "Folge den unten aufgeführten Schritten, um einen neuen Blueprint zu erstellen und anzuwenden",
|
||||
"blueprintDetails": "Blueprint Detailinformationen",
|
||||
"blueprintDetailsDescription": "Siehe das Ergebnis des angewendeten Blueprints und alle aufgetretenen Fehler",
|
||||
"blueprintInfo": "Blueprint Informationen",
|
||||
"message": "Nachricht",
|
||||
"blueprintContentsDescription": "Den YAML-Inhalt definieren, der die Infrastruktur beschreibt",
|
||||
"blueprintErrorCreateDescription": "Fehler beim Anwenden der Blaupause",
|
||||
"blueprintErrorCreate": "Fehler beim Erstellen der Blaupause",
|
||||
"searchBlueprintProgress": "Blaupausen suchen...",
|
||||
"blueprintErrorCreateDescription": "Fehler beim Anwenden des Blueprints",
|
||||
"blueprintErrorCreate": "Fehler beim Erstellen des Blueprints",
|
||||
"searchBlueprintProgress": "Blueprints suchen...",
|
||||
"appliedAt": "Angewandt am",
|
||||
"source": "Quelle",
|
||||
"contents": "Inhalt",
|
||||
"parsedContents": "Analysierte Inhalte (Nur lesen)",
|
||||
"enableDockerSocket": "Docker Blaupause aktivieren",
|
||||
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blaupausenbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
||||
"enableDockerSocket": "Docker Blueprint aktivieren",
|
||||
"enableDockerSocketDescription": "Aktiviere Docker-Socket-Label-Scraping für Blueprintbeschriftungen. Der Socket-Pfad muss neu angegeben werden.",
|
||||
"enableDockerSocketLink": "Mehr erfahren",
|
||||
"viewDockerContainers": "Docker Container anzeigen",
|
||||
"containersIn": "Container in {siteName}",
|
||||
@@ -1543,7 +1543,7 @@
|
||||
"healthCheckPathRequired": "Gesundheits-Check-Pfad ist erforderlich",
|
||||
"healthCheckMethodRequired": "HTTP-Methode ist erforderlich",
|
||||
"healthCheckIntervalMin": "Prüfintervall muss mindestens 5 Sekunden betragen",
|
||||
"healthCheckTimeoutMin": "Timeout muss mindestens 1 Sekunde betragen",
|
||||
"healthCheckTimeoutMin": "Zeitüberschreitung muss mindestens 1 Sekunde betragen",
|
||||
"healthCheckRetryMin": "Wiederholungsversuche müssen mindestens 1 betragen",
|
||||
"httpMethod": "HTTP-Methode",
|
||||
"selectHttpMethod": "HTTP-Methode auswählen",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm Password",
|
||||
"createAccount": "Create Account",
|
||||
"viewSettings": "View settings",
|
||||
"viewSettings": "View Settings",
|
||||
"delete": "Delete",
|
||||
"name": "Name",
|
||||
"online": "Online",
|
||||
@@ -51,6 +51,9 @@
|
||||
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
|
||||
"siteManageSites": "Manage Sites",
|
||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||
"sitesBannerTitle": "Connect Any Network",
|
||||
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
||||
"sitesBannerButtonText": "Install Site",
|
||||
"siteCreate": "Create Site",
|
||||
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
|
||||
"siteCreateDescription": "Create a new site to start connecting resources",
|
||||
@@ -100,6 +103,7 @@
|
||||
"siteTunnelDescription": "Determine how you want to connect to the site",
|
||||
"siteNewtCredentials": "Credentials",
|
||||
"siteNewtCredentialsDescription": "This is how the site will authenticate with the server",
|
||||
"remoteNodeCredentialsDescription": "This is how the remote node will authenticate with the server",
|
||||
"siteCredentialsSave": "Save the Credentials",
|
||||
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
||||
"siteInfo": "Site Information",
|
||||
@@ -146,8 +150,12 @@
|
||||
"shareErrorSelectResource": "Please select a resource",
|
||||
"proxyResourceTitle": "Manage Public Resources",
|
||||
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
|
||||
"proxyResourcesBannerTitle": "Web-based Public Access",
|
||||
"proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
|
||||
"clientResourceTitle": "Manage Private Resources",
|
||||
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
||||
"privateResourcesBannerTitle": "Zero-Trust Private Access",
|
||||
"privateResourcesBannerDescription": "Private resources use zero-trust security, ensuring users and machines can only access resources you explicitly grant. Connect user devices or machine clients to access these resources over a secure virtual private network.",
|
||||
"resourcesSearch": "Search resources...",
|
||||
"resourceAdd": "Add Resource",
|
||||
"resourceErrorDelte": "Error deleting resource",
|
||||
@@ -157,9 +165,9 @@
|
||||
"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?",
|
||||
"resourceHTTP": "HTTPS Resource",
|
||||
"resourceHTTPDescription": "Proxy requests to the app over HTTPS using a subdomain or base domain.",
|
||||
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
||||
"resourceRaw": "Raw TCP/UDP Resource",
|
||||
"resourceRawDescription": "Proxy requests to the app over TCP/UDP using a port number. This only works when sites are connected to nodes.",
|
||||
"resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.",
|
||||
"resourceCreate": "Create Resource",
|
||||
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
||||
"resourceSeeAll": "See All Resources",
|
||||
@@ -419,7 +427,7 @@
|
||||
"userErrorExistsDescription": "This user is already a member of the organization.",
|
||||
"inviteError": "Failed to invite user",
|
||||
"inviteErrorDescription": "An error occurred while inviting the user",
|
||||
"userInvited": "User invited",
|
||||
"userInvited": "User Invited",
|
||||
"userInvitedDescription": "The user has been successfully invited.",
|
||||
"userErrorCreate": "Failed to create user",
|
||||
"userErrorCreateDescription": "An error occurred while creating the user",
|
||||
@@ -687,7 +695,7 @@
|
||||
"resourceRoleDescription": "Admins can always access this resource.",
|
||||
"resourceUsersRoles": "Access Controls",
|
||||
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
||||
"resourceUsersRolesSubmit": "Save Users & Roles",
|
||||
"resourceUsersRolesSubmit": "Save Access Controls",
|
||||
"resourceWhitelistSave": "Saved successfully",
|
||||
"resourceWhitelistSaveDescription": "Whitelist settings have been saved",
|
||||
"ssoUse": "Use Platform SSO",
|
||||
@@ -945,7 +953,7 @@
|
||||
"pincodeAuth": "Authenticator Code",
|
||||
"pincodeSubmit2": "Submit Code",
|
||||
"passwordResetSubmit": "Request Reset",
|
||||
"passwordResetAlreadyHaveCode": "Enter Password Reset Code",
|
||||
"passwordResetAlreadyHaveCode": "Enter Code",
|
||||
"passwordResetSmtpRequired": "Please contact your administrator",
|
||||
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
|
||||
"passwordBack": "Back to Password",
|
||||
@@ -1035,6 +1043,7 @@
|
||||
"updateOrgUser": "Update Org User",
|
||||
"createOrgUser": "Create Org User",
|
||||
"actionUpdateOrg": "Update Organization",
|
||||
"actionRemoveInvitation": "Remove Invitation",
|
||||
"actionUpdateUser": "Update User",
|
||||
"actionGetUser": "Get User",
|
||||
"actionGetOrgUser": "Get Organization User",
|
||||
@@ -1044,6 +1053,8 @@
|
||||
"actionGetSite": "Get Site",
|
||||
"actionListSites": "List Sites",
|
||||
"actionApplyBlueprint": "Apply Blueprint",
|
||||
"actionListBlueprints": "List Blueprints",
|
||||
"actionGetBlueprint": "Get Blueprint",
|
||||
"setupToken": "Setup Token",
|
||||
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||
"setupTokenRequired": "Setup token is required",
|
||||
@@ -1194,7 +1205,7 @@
|
||||
"sidebarUserDevices": "Users",
|
||||
"sidebarMachineClients": "Machines",
|
||||
"sidebarDomains": "Domains",
|
||||
"sidebarGeneral": "General",
|
||||
"sidebarGeneral": "Manage",
|
||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||
"sidebarBluePrints": "Blueprints",
|
||||
"sidebarOrganization": "Organization",
|
||||
@@ -1308,8 +1319,11 @@
|
||||
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
|
||||
"documentation": "Documentation",
|
||||
"saveAllSettings": "Save All Settings",
|
||||
"saveResourceTargets": "Save Targets",
|
||||
"saveResourceHttp": "Save Proxy Settings",
|
||||
"saveProxyProtocol": "Save Proxy protocol settings",
|
||||
"settingsUpdated": "Settings updated",
|
||||
"settingsUpdatedDescription": "All settings have been updated successfully",
|
||||
"settingsUpdatedDescription": "Settings updated successfully",
|
||||
"settingsErrorUpdate": "Failed to update settings",
|
||||
"settingsErrorUpdateDescription": "An error occurred while updating settings",
|
||||
"sidebarCollapse": "Collapse",
|
||||
@@ -1616,9 +1630,8 @@
|
||||
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
||||
"createInternalResourceDialogName": "Name",
|
||||
"createInternalResourceDialogSite": "Site",
|
||||
"createInternalResourceDialogSelectSite": "Select site...",
|
||||
"createInternalResourceDialogSearchSites": "Search sites...",
|
||||
"createInternalResourceDialogNoSitesFound": "No sites found.",
|
||||
"selectSite": "Select site...",
|
||||
"noSitesFound": "No sites found.",
|
||||
"createInternalResourceDialogProtocol": "Protocol",
|
||||
"createInternalResourceDialogTcp": "TCP",
|
||||
"createInternalResourceDialogUdp": "UDP",
|
||||
@@ -1658,7 +1671,7 @@
|
||||
"siteAddressDescription": "The internal address of the site. Must fall within the organization's subnet.",
|
||||
"siteNameDescription": "The display name of the site that can be changed later.",
|
||||
"autoLoginExternalIdp": "Auto Login with External IDP",
|
||||
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.",
|
||||
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external identity provider for authentication.",
|
||||
"selectIdp": "Select IDP",
|
||||
"selectIdpPlaceholder": "Choose an IDP...",
|
||||
"selectIdpRequired": "Please select an IDP when auto login is enabled.",
|
||||
@@ -1670,7 +1683,7 @@
|
||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud",
|
||||
"remoteExitNodeDescription": "Self-host your own remote relay and proxy server nodes",
|
||||
"remoteExitNodes": "Nodes",
|
||||
"searchRemoteExitNodes": "Search nodes...",
|
||||
"remoteExitNodeAdd": "Add Node",
|
||||
@@ -1680,20 +1693,22 @@
|
||||
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
||||
"remoteExitNodeDelete": "Delete Node",
|
||||
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||
"remoteExitNodeId": "ID",
|
||||
"remoteExitNodeSecretKey": "Secret",
|
||||
"remoteExitNodeCreate": {
|
||||
"title": "Create Node",
|
||||
"description": "Create a new node to extend network connectivity",
|
||||
"title": "Create Remote Node",
|
||||
"description": "Create a new self-hosted remote relay and proxy server node",
|
||||
"viewAllButton": "View All Nodes",
|
||||
"strategy": {
|
||||
"title": "Creation Strategy",
|
||||
"description": "Choose this to manually configure the node or generate new credentials.",
|
||||
"description": "Select how you want to create the remote node",
|
||||
"adopt": {
|
||||
"title": "Adopt Node",
|
||||
"description": "Choose this if you already have the credentials for the node."
|
||||
},
|
||||
"generate": {
|
||||
"title": "Generate Keys",
|
||||
"description": "Choose this if you want to generate new keys for the node"
|
||||
"description": "Choose this if you want to generate new keys for the node."
|
||||
}
|
||||
},
|
||||
"adopt": {
|
||||
@@ -1806,9 +1821,30 @@
|
||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||
"subnet": "Subnet",
|
||||
"subnetDescription": "The subnet for this organization's network configuration.",
|
||||
"authPage": "Auth Page",
|
||||
"authPageDescription": "Configure the auth page for the organization",
|
||||
"customDomain": "Custom Domain",
|
||||
"authPage": "Authentication Pages",
|
||||
"authPageDescription": "Set a custom domain for the organization's authentication pages",
|
||||
"authPageDomain": "Auth Page Domain",
|
||||
"authPageBranding": "Custom Branding",
|
||||
"authPageBrandingDescription": "Configure the branding that appears on authentication pages for this organization",
|
||||
"authPageBrandingUpdated": "Auth page Branding updated successfully",
|
||||
"authPageBrandingRemoved": "Auth page Branding removed successfully",
|
||||
"authPageBrandingRemoveTitle": "Remove Auth Page Branding",
|
||||
"authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?",
|
||||
"authPageBrandingDeleteConfirm": "Confirm Delete Branding",
|
||||
"brandingLogoURL": "Logo URL",
|
||||
"brandingPrimaryColor": "Primary Color",
|
||||
"brandingLogoWidth": "Width (px)",
|
||||
"brandingLogoHeight": "Height (px)",
|
||||
"brandingOrgTitle": "Title for Organization Auth Page",
|
||||
"brandingOrgDescription": "{orgName} will be replaced with the organization's name",
|
||||
"brandingOrgSubtitle": "Subtitle for Organization Auth Page",
|
||||
"brandingResourceTitle": "Title for Resource Auth Page",
|
||||
"brandingResourceSubtitle": "Subtitle for Resource Auth Page",
|
||||
"brandingResourceDescription": "{resourceName} will be replaced with the organization's name",
|
||||
"saveAuthPageDomain": "Save Domain",
|
||||
"saveAuthPageBranding": "Save Branding",
|
||||
"removeAuthPageBranding": "Remove Branding",
|
||||
"noDomainSet": "No domain set",
|
||||
"changeDomain": "Change Domain",
|
||||
"selectDomain": "Select Domain",
|
||||
@@ -1817,7 +1853,7 @@
|
||||
"setAuthPageDomain": "Set Auth Page Domain",
|
||||
"failedToFetchCertificate": "Failed to fetch certificate",
|
||||
"failedToRestartCertificate": "Failed to restart certificate",
|
||||
"addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for the organization",
|
||||
"addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.",
|
||||
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
||||
"domainPickerProvidedDomain": "Provided Domain",
|
||||
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
||||
@@ -1832,10 +1868,19 @@
|
||||
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
|
||||
"domainPickerSubdomainSanitized": "Subdomain sanitized",
|
||||
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
|
||||
"orgAuthSignInTitle": "Sign in to the organization",
|
||||
"orgAuthSignInTitle": "Organization Sign In",
|
||||
"orgAuthChooseIdpDescription": "Choose your identity provider to continue",
|
||||
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
|
||||
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
|
||||
"orgAuthSignInToOrg": "Sign in to an organization",
|
||||
"orgAuthSelectOrgTitle": "Organization Sign In",
|
||||
"orgAuthSelectOrgDescription": "Enter your organization ID to continue",
|
||||
"orgAuthOrgIdPlaceholder": "your-organization",
|
||||
"orgAuthOrgIdHelp": "Enter your organization's unique identifier",
|
||||
"orgAuthSelectOrgHelp": "After entering your organization ID, you'll be taken to your organization's sign-in page where you can use SSO or your organization credentials.",
|
||||
"orgAuthRememberOrgId": "Remember this organization ID",
|
||||
"orgAuthBackToSignIn": "Back to standard sign in",
|
||||
"orgAuthNoAccount": "Don't have an account?",
|
||||
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
||||
"idpDisabled": "Identity providers are disabled.",
|
||||
"orgAuthPageDisabled": "Organization auth page is disabled.",
|
||||
@@ -1850,6 +1895,8 @@
|
||||
"enableTwoFactorAuthentication": "Enable two-factor authentication",
|
||||
"completeSecuritySteps": "Complete Security Steps",
|
||||
"securitySettings": "Security Settings",
|
||||
"dangerSection": "Danger Zone",
|
||||
"dangerSectionDescription": "Permanently delete all data associated with this organization",
|
||||
"securitySettingsDescription": "Configure security policies for the organization",
|
||||
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users",
|
||||
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
|
||||
@@ -1887,7 +1934,7 @@
|
||||
"securityPolicyChangeWarningText": "This will affect all users in the organization",
|
||||
"authPageErrorUpdateMessage": "An error occurred while updating the auth page settings",
|
||||
"authPageErrorUpdate": "Unable to update auth page",
|
||||
"authPageUpdated": "Auth page updated successfully",
|
||||
"authPageDomainUpdated": "Auth page Domain updated successfully",
|
||||
"healthCheckNotAvailable": "Local",
|
||||
"rewritePath": "Rewrite Path",
|
||||
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
|
||||
@@ -1915,8 +1962,15 @@
|
||||
"beta": "Beta",
|
||||
"manageUserDevices": "User Devices",
|
||||
"manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources",
|
||||
"downloadClientBannerTitle": "Download Pangolin Client",
|
||||
"downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately.",
|
||||
"manageMachineClients": "Manage Machine Clients",
|
||||
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
|
||||
"machineClientsBannerTitle": "Servers & Automated Systems",
|
||||
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
|
||||
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||
"machineClientsBannerOlmContainer": "Olm Container",
|
||||
"clientsTableUserClients": "User",
|
||||
"clientsTableMachineClients": "Machine",
|
||||
"licenseTableValidUntil": "Valid Until",
|
||||
@@ -2060,13 +2114,15 @@
|
||||
"request": "Request",
|
||||
"requests": "Requests",
|
||||
"logs": "Logs",
|
||||
"logsSettingsDescription": "Monitor logs collected from this orginization",
|
||||
"logsSettingsDescription": "Monitor logs collected from this organization",
|
||||
"searchLogs": "Search logs...",
|
||||
"action": "Action",
|
||||
"actor": "Actor",
|
||||
"timestamp": "Timestamp",
|
||||
"accessLogs": "Access Logs",
|
||||
"exportCsv": "Export CSV",
|
||||
"exportError": "Unknown error when exporting CSV",
|
||||
"exportCsvTooltip": "Within Time Range",
|
||||
"actorId": "Actor ID",
|
||||
"allowedByRule": "Allowed by Rule",
|
||||
"allowedNoAuth": "Allowed No Auth",
|
||||
@@ -2120,7 +2176,7 @@
|
||||
"unverified": "Unverified",
|
||||
"domainSetting": "Domain Settings",
|
||||
"domainSettingDescription": "Configure settings for the domain",
|
||||
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).",
|
||||
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (requires a properly configured certificate resolver).",
|
||||
"recordName": "Record Name",
|
||||
"auto": "Auto",
|
||||
"TTL": "TTL",
|
||||
@@ -2255,6 +2311,8 @@
|
||||
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
||||
"setupSubnetAdvanced": "Subnet (Advanced)",
|
||||
"setupSubnetDescription": "The subnet for this organization's internal network.",
|
||||
"setupUtilitySubnet": "Utility Subnet (Advanced)",
|
||||
"setupUtilitySubnetDescription": "The subnet for this organization's alias addresses and DNS server.",
|
||||
"siteRegenerateAndDisconnect": "Regenerate and Disconnect",
|
||||
"siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?",
|
||||
"siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.",
|
||||
@@ -2270,5 +2328,40 @@
|
||||
"remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.",
|
||||
"remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?",
|
||||
"remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.",
|
||||
"agent": "Agent"
|
||||
"agent": "Agent",
|
||||
"personalUseOnly": "Personal Use Only",
|
||||
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
|
||||
"instanceIsUnlicensed": "This instance is unlicensed.",
|
||||
"portRestrictions": "Port Restrictions",
|
||||
"allPorts": "All",
|
||||
"custom": "Custom",
|
||||
"allPortsAllowed": "All Ports Allowed",
|
||||
"allPortsBlocked": "All Ports Blocked",
|
||||
"tcpPortsDescription": "Specify which TCP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 80,443,8000-9000).",
|
||||
"udpPortsDescription": "Specify which UDP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 53,123,500-600).",
|
||||
"organizationLoginPageTitle": "Organization Login Page",
|
||||
"organizationLoginPageDescription": "Customize the login page for this organization",
|
||||
"resourceLoginPageTitle": "Resource Login Page",
|
||||
"resourceLoginPageDescription": "Customize the login page for individual resources",
|
||||
"enterConfirmation": "Enter confirmation",
|
||||
"blueprintViewDetails": "Details",
|
||||
"defaultIdentityProvider": "Default Identity Provider",
|
||||
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
||||
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
||||
"editInternalResourceDialogAddRoles": "Add Roles",
|
||||
"editInternalResourceDialogAddUsers": "Add Users",
|
||||
"editInternalResourceDialogAddClients": "Add Clients",
|
||||
"editInternalResourceDialogDestinationLabel": "Destination",
|
||||
"editInternalResourceDialogDestinationDescription": "Specify the destination address for the internal resource. This can be a hostname, IP address, or CIDR range depending on the selected mode. Optionally set an internal DNS alias for easier identification.",
|
||||
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
|
||||
"editInternalResourceDialogTcp": "TCP",
|
||||
"editInternalResourceDialogUdp": "UDP",
|
||||
"editInternalResourceDialogIcmp": "ICMP",
|
||||
"editInternalResourceDialogAccessControl": "Access Control",
|
||||
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "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.",
|
||||
"orgAuthWhatsThis": "Where can I find my organization ID?",
|
||||
"learnMore": "Learn more",
|
||||
"backToHome": "Go back to home",
|
||||
"needToSignInToOrg": "Need to use your organization's identity provider?"
|
||||
}
|
||||
|
||||
@@ -1022,6 +1022,8 @@
|
||||
"actionGetSite": "獲取站點",
|
||||
"actionListSites": "站點列表",
|
||||
"actionApplyBlueprint": "應用藍圖",
|
||||
"actionListBlueprints": "藍圖列表",
|
||||
"actionGetBlueprint": "獲取藍圖",
|
||||
"setupToken": "設置令牌",
|
||||
"setupTokenDescription": "從伺服器控制台輸入設定令牌。",
|
||||
"setupTokenRequired": "需要設置令牌",
|
||||
|
||||
@@ -4,6 +4,7 @@ import createNextIntlPlugin from "next-intl/plugin";
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: false,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true
|
||||
},
|
||||
|
||||
6097
package-lock.json
generated
179
package.json
@@ -19,9 +19,9 @@
|
||||
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
||||
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
||||
"db:clear-migrations": "rm -rf server/migrations",
|
||||
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||
"set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||
"set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||
"set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
|
||||
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
|
||||
"next:build": "next build",
|
||||
@@ -29,16 +29,17 @@
|
||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
||||
"email": "email dev --dir server/emails/templates --port 3005",
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "8.1.0",
|
||||
"@faker-js/faker": "^10.1.0",
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@aws-sdk/client-s3": "3.943.0",
|
||||
"@asteasolutions/zod-to-openapi": "8.2.0",
|
||||
"@aws-sdk/client-s3": "3.955.0",
|
||||
"@faker-js/faker": "10.1.0",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@node-rs/argon2": "2.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@radix-ui/react-avatar": "1.1.11",
|
||||
@@ -49,138 +50,132 @@
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-label": "2.1.8",
|
||||
"@radix-ui/react-popover": "1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-progress": "1.1.8",
|
||||
"@radix-ui/react-radio-group": "1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-scroll-area": "1.2.10",
|
||||
"@radix-ui/react-select": "2.2.6",
|
||||
"@radix-ui/react-separator": "1.1.8",
|
||||
"@radix-ui/react-slot": "1.2.4",
|
||||
"@radix-ui/react-switch": "1.2.6",
|
||||
"@radix-ui/react-tabs": "1.1.13",
|
||||
"@radix-ui/react-toast": "1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-email/components": "0.5.7",
|
||||
"@react-email/render": "^1.3.2",
|
||||
"@react-email/tailwind": "1.2.2",
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@simplewebauthn/server": "^13.2.2",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tanstack/react-query": "^5.90.6",
|
||||
"@radix-ui/react-tooltip": "1.2.8",
|
||||
"@react-email/components": "1.0.2",
|
||||
"@react-email/render": "2.0.0",
|
||||
"@react-email/tailwind": "2.0.2",
|
||||
"@simplewebauthn/browser": "13.2.2",
|
||||
"@simplewebauthn/server": "13.2.2",
|
||||
"@tailwindcss/forms": "0.5.11",
|
||||
"@tanstack/react-query": "5.90.12",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"arctic": "^3.7.0",
|
||||
"axios": "^1.13.2",
|
||||
"better-sqlite3": "11.7.0",
|
||||
"arctic": "3.7.0",
|
||||
"axios": "1.13.2",
|
||||
"better-sqlite3": "11.9.1",
|
||||
"canvas-confetti": "1.9.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie": "1.1.1",
|
||||
"cookie-parser": "1.4.7",
|
||||
"cookies": "^0.9.1",
|
||||
"cookies": "0.9.1",
|
||||
"cors": "2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"crypto-js": "4.2.0",
|
||||
"d3": "7.9.0",
|
||||
"date-fns": "4.1.0",
|
||||
"drizzle-orm": "0.45.0",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-next": "16.0.7",
|
||||
"drizzle-orm": "0.45.1",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-next": "16.1.0",
|
||||
"express": "5.2.1",
|
||||
"express-rate-limit": "8.2.1",
|
||||
"glob": "11.1.0",
|
||||
"glob": "13.0.0",
|
||||
"helmet": "8.1.0",
|
||||
"http-errors": "2.0.1",
|
||||
"i": "^0.3.7",
|
||||
"i": "0.3.7",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.8.2",
|
||||
"jmespath": "^0.16.0",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.556.0",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lucide-react": "0.562.0",
|
||||
"maxmind": "5.0.1",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.7",
|
||||
"next-intl": "^4.4.0",
|
||||
"next": "15.5.9",
|
||||
"next-intl": "4.6.1",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "^3.9.17",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "7.0.11",
|
||||
"npm": "^11.6.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"npm": "11.7.0",
|
||||
"nprogress": "0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "^8.16.2",
|
||||
"posthog-node": "^5.11.2",
|
||||
"pg": "8.16.3",
|
||||
"posthog-node": "5.17.4",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.1",
|
||||
"react-day-picker": "9.11.3",
|
||||
"react-dom": "19.2.1",
|
||||
"react-easy-sort": "^1.8.0",
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "9.13.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.68.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-icons": "5.5.0",
|
||||
"rebuild": "0.1.2",
|
||||
"recharts": "^2.15.4",
|
||||
"reodotdev": "^1.0.0",
|
||||
"resend": "^6.4.2",
|
||||
"semver": "^7.7.3",
|
||||
"stripe": "18.2.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"topojson-client": "^3.1.0",
|
||||
"recharts": "2.15.4",
|
||||
"reodotdev": "1.0.0",
|
||||
"resend": "6.6.0",
|
||||
"semver": "7.7.3",
|
||||
"stripe": "20.1.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"uuid": "^13.0.0",
|
||||
"topojson-client": "3.1.0",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"uuid": "13.0.0",
|
||||
"vaul": "1.1.2",
|
||||
"visionscarto-world-atlas": "^1.0.0",
|
||||
"winston": "3.18.3",
|
||||
"visionscarto-world-atlas": "1.0.0",
|
||||
"winston": "3.19.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.3",
|
||||
"yaml": "^2.8.1",
|
||||
"yaml": "2.8.2",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "4.1.12",
|
||||
"zod": "4.2.1",
|
||||
"zod-validation-error": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.51.1",
|
||||
"@dotenvx/dotenvx": "1.51.2",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@react-email/preview-server": "4.3.2",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tanstack/react-query-devtools": "5.91.1",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/crypto-js": "4.2.2",
|
||||
"@types/d3": "7.4.3",
|
||||
"@types/express": "5.0.6",
|
||||
"@types/express-session": "^1.18.2",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/express-session": "1.18.2",
|
||||
"@types/jmespath": "0.15.2",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "24.10.2",
|
||||
"@types/nodemailer": "7.0.4",
|
||||
"@types/pg": "8.15.6",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@types/pg": "8.16.0",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/swagger-ui-express": "^4.1.8",
|
||||
"@types/topojson-client": "^3.1.5",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/topojson-client": "3.1.5",
|
||||
"@types/ws": "8.18.1",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"@types/yargs": "17.0.35",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"drizzle-kit": "0.31.8",
|
||||
"esbuild": "0.27.1",
|
||||
"esbuild": "0.27.2",
|
||||
"esbuild-node-externals": "1.20.1",
|
||||
"postcss": "^8",
|
||||
"react-email": "4.3.2",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.7.4",
|
||||
"react-email": "5.0.7",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "^5",
|
||||
"typescript-eslint": "^8.46.3"
|
||||
},
|
||||
"overrides": {
|
||||
"emblor": {
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
}
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.49.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
"@tailwindcss/postcss": {}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
|
Before Width: | Height: | Size: 687 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 636 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 484 KiB |
BIN
public/screenshots/private-resources.png
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
public/screenshots/public-resources.png
Normal file
|
After Width: | Height: | Size: 484 KiB |
|
Before Width: | Height: | Size: 713 KiB |
|
Before Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 674 KiB After Width: | Height: | Size: 396 KiB |
BIN
public/screenshots/user-devices.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
@@ -2,13 +2,13 @@ import { hash, verify } from "@node-rs/argon2";
|
||||
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hash: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
const validPassword = await verify(hash, password, {
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1,
|
||||
parallelism: 1
|
||||
});
|
||||
return validPassword;
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export async function hashPassword(password: string): Promise<string> {
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1,
|
||||
parallelism: 1
|
||||
});
|
||||
|
||||
return passwordHash;
|
||||
|
||||
@@ -4,10 +4,13 @@ export const passwordSchema = z
|
||||
.string()
|
||||
.min(8, { message: "Password must be at least 8 characters long" })
|
||||
.max(128, { message: "Password must be at most 128 characters long" })
|
||||
.regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/, {
|
||||
message: `Your password must meet the following conditions:
|
||||
.regex(
|
||||
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/,
|
||||
{
|
||||
message: `Your password must meet the following conditions:
|
||||
at least one uppercase English letter,
|
||||
at least one lowercase English letter,
|
||||
at least one digit,
|
||||
at least one special character.`
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
encodeHexLowerCase,
|
||||
} from "@oslojs/encoding";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Newt, newts, newtSessions, NewtSession } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
@@ -10,25 +8,25 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
export async function createNewtSession(
|
||||
token: string,
|
||||
newtId: string,
|
||||
newtId: string
|
||||
): Promise<NewtSession> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const session: NewtSession = {
|
||||
sessionId: sessionId,
|
||||
newtId,
|
||||
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
|
||||
expiresAt: new Date(Date.now() + EXPIRES).getTime()
|
||||
};
|
||||
await db.insert(newtSessions).values(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function validateNewtSessionToken(
|
||||
token: string,
|
||||
token: string
|
||||
): Promise<SessionValidationResult> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const result = await db
|
||||
.select({ newt: newts, session: newtSessions })
|
||||
@@ -45,14 +43,12 @@ export async function validateNewtSessionToken(
|
||||
.where(eq(newtSessions.sessionId, session.sessionId));
|
||||
return { session: null, newt: null };
|
||||
}
|
||||
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
|
||||
session.expiresAt = new Date(
|
||||
Date.now() + EXPIRES,
|
||||
).getTime();
|
||||
if (Date.now() >= session.expiresAt - EXPIRES / 2) {
|
||||
session.expiresAt = new Date(Date.now() + EXPIRES).getTime();
|
||||
await db
|
||||
.update(newtSessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt,
|
||||
expiresAt: session.expiresAt
|
||||
})
|
||||
.where(eq(newtSessions.sessionId, session.sessionId));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
encodeHexLowerCase,
|
||||
} from "@oslojs/encoding";
|
||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||
import { sha256 } from "@oslojs/crypto/sha2";
|
||||
import { Olm, olms, olmSessions, OlmSession } from "@server/db";
|
||||
import { db } from "@server/db";
|
||||
@@ -10,25 +8,25 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
||||
|
||||
export async function createOlmSession(
|
||||
token: string,
|
||||
olmId: string,
|
||||
olmId: string
|
||||
): Promise<OlmSession> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const session: OlmSession = {
|
||||
sessionId: sessionId,
|
||||
olmId,
|
||||
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
|
||||
expiresAt: new Date(Date.now() + EXPIRES).getTime()
|
||||
};
|
||||
await db.insert(olmSessions).values(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function validateOlmSessionToken(
|
||||
token: string,
|
||||
token: string
|
||||
): Promise<SessionValidationResult> {
|
||||
const sessionId = encodeHexLowerCase(
|
||||
sha256(new TextEncoder().encode(token)),
|
||||
sha256(new TextEncoder().encode(token))
|
||||
);
|
||||
const result = await db
|
||||
.select({ olm: olms, session: olmSessions })
|
||||
@@ -45,14 +43,12 @@ export async function validateOlmSessionToken(
|
||||
.where(eq(olmSessions.sessionId, session.sessionId));
|
||||
return { session: null, olm: null };
|
||||
}
|
||||
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
|
||||
session.expiresAt = new Date(
|
||||
Date.now() + EXPIRES,
|
||||
).getTime();
|
||||
if (Date.now() >= session.expiresAt - EXPIRES / 2) {
|
||||
session.expiresAt = new Date(Date.now() + EXPIRES).getTime();
|
||||
await db
|
||||
.update(olmSessions)
|
||||
.set({
|
||||
expiresAt: session.expiresAt,
|
||||
expiresAt: session.expiresAt
|
||||
})
|
||||
.where(eq(olmSessions.sessionId, session.sessionId));
|
||||
}
|
||||
|
||||
@@ -10,4 +10,4 @@ export async function initCleanup() {
|
||||
// Handle process termination
|
||||
process.on("SIGTERM", () => cleanup());
|
||||
process.on("SIGINT", () => cleanup());
|
||||
}
|
||||
}
|
||||
|
||||
321
server/db/asns.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
// Curated list of major ASNs (Cloud Providers, CDNs, ISPs, etc.)
|
||||
// This is not exhaustive - there are 100,000+ ASNs globally
|
||||
// Users can still enter any ASN manually in the input field
|
||||
export const MAJOR_ASNS = [
|
||||
{
|
||||
name: "ALL ASNs",
|
||||
code: "ALL",
|
||||
asn: 0 // Special value that will match all
|
||||
},
|
||||
// Major Cloud Providers
|
||||
{
|
||||
name: "Google LLC",
|
||||
code: "AS15169",
|
||||
asn: 15169
|
||||
},
|
||||
{
|
||||
name: "Amazon AWS",
|
||||
code: "AS16509",
|
||||
asn: 16509
|
||||
},
|
||||
{
|
||||
name: "Amazon AWS (EC2)",
|
||||
code: "AS14618",
|
||||
asn: 14618
|
||||
},
|
||||
{
|
||||
name: "Microsoft Azure",
|
||||
code: "AS8075",
|
||||
asn: 8075
|
||||
},
|
||||
{
|
||||
name: "Microsoft Corporation",
|
||||
code: "AS8068",
|
||||
asn: 8068
|
||||
},
|
||||
{
|
||||
name: "DigitalOcean",
|
||||
code: "AS14061",
|
||||
asn: 14061
|
||||
},
|
||||
{
|
||||
name: "Linode",
|
||||
code: "AS63949",
|
||||
asn: 63949
|
||||
},
|
||||
{
|
||||
name: "Hetzner Online",
|
||||
code: "AS24940",
|
||||
asn: 24940
|
||||
},
|
||||
{
|
||||
name: "OVH SAS",
|
||||
code: "AS16276",
|
||||
asn: 16276
|
||||
},
|
||||
{
|
||||
name: "Oracle Cloud",
|
||||
code: "AS31898",
|
||||
asn: 31898
|
||||
},
|
||||
{
|
||||
name: "Alibaba Cloud",
|
||||
code: "AS45102",
|
||||
asn: 45102
|
||||
},
|
||||
{
|
||||
name: "IBM Cloud",
|
||||
code: "AS36351",
|
||||
asn: 36351
|
||||
},
|
||||
|
||||
// CDNs
|
||||
{
|
||||
name: "Cloudflare",
|
||||
code: "AS13335",
|
||||
asn: 13335
|
||||
},
|
||||
{
|
||||
name: "Fastly",
|
||||
code: "AS54113",
|
||||
asn: 54113
|
||||
},
|
||||
{
|
||||
name: "Akamai Technologies",
|
||||
code: "AS20940",
|
||||
asn: 20940
|
||||
},
|
||||
{
|
||||
name: "Akamai (Primary)",
|
||||
code: "AS16625",
|
||||
asn: 16625
|
||||
},
|
||||
|
||||
// Mobile Carriers - US
|
||||
{
|
||||
name: "T-Mobile USA",
|
||||
code: "AS21928",
|
||||
asn: 21928
|
||||
},
|
||||
{
|
||||
name: "Verizon Wireless",
|
||||
code: "AS6167",
|
||||
asn: 6167
|
||||
},
|
||||
{
|
||||
name: "AT&T Mobility",
|
||||
code: "AS20057",
|
||||
asn: 20057
|
||||
},
|
||||
{
|
||||
name: "Sprint (T-Mobile)",
|
||||
code: "AS1239",
|
||||
asn: 1239
|
||||
},
|
||||
{
|
||||
name: "US Cellular",
|
||||
code: "AS6430",
|
||||
asn: 6430
|
||||
},
|
||||
|
||||
// Mobile Carriers - Europe
|
||||
{
|
||||
name: "Vodafone UK",
|
||||
code: "AS25135",
|
||||
asn: 25135
|
||||
},
|
||||
{
|
||||
name: "EE (UK)",
|
||||
code: "AS12576",
|
||||
asn: 12576
|
||||
},
|
||||
{
|
||||
name: "Three UK",
|
||||
code: "AS29194",
|
||||
asn: 29194
|
||||
},
|
||||
{
|
||||
name: "O2 UK",
|
||||
code: "AS13285",
|
||||
asn: 13285
|
||||
},
|
||||
{
|
||||
name: "Telefonica Spain Mobile",
|
||||
code: "AS12430",
|
||||
asn: 12430
|
||||
},
|
||||
|
||||
// Mobile Carriers - Asia
|
||||
{
|
||||
name: "NTT DoCoMo (Japan)",
|
||||
code: "AS9605",
|
||||
asn: 9605
|
||||
},
|
||||
{
|
||||
name: "SoftBank Mobile (Japan)",
|
||||
code: "AS17676",
|
||||
asn: 17676
|
||||
},
|
||||
{
|
||||
name: "SK Telecom (Korea)",
|
||||
code: "AS9318",
|
||||
asn: 9318
|
||||
},
|
||||
{
|
||||
name: "KT Corporation Mobile (Korea)",
|
||||
code: "AS4766",
|
||||
asn: 4766
|
||||
},
|
||||
{
|
||||
name: "Airtel India",
|
||||
code: "AS24560",
|
||||
asn: 24560
|
||||
},
|
||||
{
|
||||
name: "China Mobile",
|
||||
code: "AS9808",
|
||||
asn: 9808
|
||||
},
|
||||
|
||||
// Major US ISPs
|
||||
{
|
||||
name: "AT&T Services",
|
||||
code: "AS7018",
|
||||
asn: 7018
|
||||
},
|
||||
{
|
||||
name: "Comcast Cable",
|
||||
code: "AS7922",
|
||||
asn: 7922
|
||||
},
|
||||
{
|
||||
name: "Verizon",
|
||||
code: "AS701",
|
||||
asn: 701
|
||||
},
|
||||
{
|
||||
name: "Cox Communications",
|
||||
code: "AS22773",
|
||||
asn: 22773
|
||||
},
|
||||
{
|
||||
name: "Charter Communications",
|
||||
code: "AS20115",
|
||||
asn: 20115
|
||||
},
|
||||
{
|
||||
name: "CenturyLink",
|
||||
code: "AS209",
|
||||
asn: 209
|
||||
},
|
||||
|
||||
// Major European ISPs
|
||||
{
|
||||
name: "Deutsche Telekom",
|
||||
code: "AS3320",
|
||||
asn: 3320
|
||||
},
|
||||
{
|
||||
name: "Vodafone",
|
||||
code: "AS1273",
|
||||
asn: 1273
|
||||
},
|
||||
{
|
||||
name: "British Telecom",
|
||||
code: "AS2856",
|
||||
asn: 2856
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
code: "AS3215",
|
||||
asn: 3215
|
||||
},
|
||||
{
|
||||
name: "Telefonica",
|
||||
code: "AS12956",
|
||||
asn: 12956
|
||||
},
|
||||
|
||||
// Major Asian ISPs
|
||||
{
|
||||
name: "China Telecom",
|
||||
code: "AS4134",
|
||||
asn: 4134
|
||||
},
|
||||
{
|
||||
name: "China Unicom",
|
||||
code: "AS4837",
|
||||
asn: 4837
|
||||
},
|
||||
{
|
||||
name: "NTT Communications",
|
||||
code: "AS2914",
|
||||
asn: 2914
|
||||
},
|
||||
{
|
||||
name: "KDDI Corporation",
|
||||
code: "AS2516",
|
||||
asn: 2516
|
||||
},
|
||||
{
|
||||
name: "Reliance Jio (India)",
|
||||
code: "AS55836",
|
||||
asn: 55836
|
||||
},
|
||||
|
||||
// VPN/Proxy Providers
|
||||
{
|
||||
name: "Private Internet Access",
|
||||
code: "AS46562",
|
||||
asn: 46562
|
||||
},
|
||||
{
|
||||
name: "NordVPN",
|
||||
code: "AS202425",
|
||||
asn: 202425
|
||||
},
|
||||
{
|
||||
name: "Mullvad VPN",
|
||||
code: "AS213281",
|
||||
asn: 213281
|
||||
},
|
||||
|
||||
// Social Media / Major Tech
|
||||
{
|
||||
name: "Facebook/Meta",
|
||||
code: "AS32934",
|
||||
asn: 32934
|
||||
},
|
||||
{
|
||||
name: "Twitter/X",
|
||||
code: "AS13414",
|
||||
asn: 13414
|
||||
},
|
||||
{
|
||||
name: "Apple",
|
||||
code: "AS714",
|
||||
asn: 714
|
||||
},
|
||||
{
|
||||
name: "Netflix",
|
||||
code: "AS2906",
|
||||
asn: 2906
|
||||
},
|
||||
|
||||
// Academic/Research
|
||||
{
|
||||
name: "MIT",
|
||||
code: "AS3",
|
||||
asn: 3
|
||||
},
|
||||
{
|
||||
name: "Stanford University",
|
||||
code: "AS32",
|
||||
asn: 32
|
||||
},
|
||||
{
|
||||
name: "CERN",
|
||||
code: "AS513",
|
||||
asn: 513
|
||||
}
|
||||
];
|
||||
13
server/db/maxmindAsn.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import maxmind, { AsnResponse, Reader } from "maxmind";
|
||||
import config from "@server/lib/config";
|
||||
|
||||
let maxmindAsnLookup: Reader<AsnResponse> | null;
|
||||
if (config.getRawConfig().server.maxmind_asn_path) {
|
||||
maxmindAsnLookup = await maxmind.open<AsnResponse>(
|
||||
config.getRawConfig().server.maxmind_asn_path!
|
||||
);
|
||||
} else {
|
||||
maxmindAsnLookup = null;
|
||||
}
|
||||
|
||||
export { maxmindAsnLookup };
|
||||
@@ -1708,4 +1708,4 @@
|
||||
"Desert Box Turtle",
|
||||
"African Striped Weasel"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,28 +6,28 @@ import { withReplicas } from "drizzle-orm/pg-core";
|
||||
function createDb() {
|
||||
const config = readConfigFile();
|
||||
|
||||
if (!config.postgres) {
|
||||
// check the environment variables for postgres config
|
||||
if (process.env.POSTGRES_CONNECTION_STRING) {
|
||||
config.postgres = {
|
||||
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
||||
};
|
||||
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
||||
const replicas =
|
||||
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(
|
||||
","
|
||||
).map((conn) => ({
|
||||
// check the environment variables for postgres config first before the config file
|
||||
if (process.env.POSTGRES_CONNECTION_STRING) {
|
||||
config.postgres = {
|
||||
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
||||
};
|
||||
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
||||
const replicas =
|
||||
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map(
|
||||
(conn) => ({
|
||||
connection_string: conn.trim()
|
||||
}));
|
||||
config.postgres.replicas = replicas;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"Postgres configuration is missing in the configuration file."
|
||||
);
|
||||
})
|
||||
);
|
||||
config.postgres.replicas = replicas;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.postgres) {
|
||||
throw new Error(
|
||||
"Postgres configuration is missing in the configuration file."
|
||||
);
|
||||
}
|
||||
|
||||
const connectionString = config.postgres?.connection_string;
|
||||
const replicaConnections = config.postgres?.replicas || [];
|
||||
|
||||
@@ -81,6 +81,7 @@ function createDb() {
|
||||
|
||||
export const db = createDb();
|
||||
export default db;
|
||||
export const primaryDb = db.$primary;
|
||||
export type Transaction = Parameters<
|
||||
Parameters<(typeof db)["transaction"]>[0]
|
||||
>[0];
|
||||
|
||||
@@ -10,7 +10,7 @@ const runMigrations = async () => {
|
||||
await migrate(db as any, {
|
||||
migrationsFolder: migrationsFolder
|
||||
});
|
||||
console.log("Migrations completed successfully.");
|
||||
console.log("Migrations completed successfully. ✅");
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error running migrations:", error);
|
||||
|
||||
@@ -204,6 +204,29 @@ export const loginPageOrg = pgTable("loginPageOrg", {
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const loginPageBranding = pgTable("loginPageBranding", {
|
||||
loginPageBrandingId: serial("loginPageBrandingId").primaryKey(),
|
||||
logoUrl: text("logoUrl").notNull(),
|
||||
logoWidth: integer("logoWidth").notNull(),
|
||||
logoHeight: integer("logoHeight").notNull(),
|
||||
primaryColor: text("primaryColor"),
|
||||
resourceTitle: text("resourceTitle").notNull(),
|
||||
resourceSubtitle: text("resourceSubtitle"),
|
||||
orgTitle: text("orgTitle"),
|
||||
orgSubtitle: text("orgSubtitle")
|
||||
});
|
||||
|
||||
export const loginPageBrandingOrg = pgTable("loginPageBrandingOrg", {
|
||||
loginPageBrandingId: integer("loginPageBrandingId")
|
||||
.notNull()
|
||||
.references(() => loginPageBranding.loginPageBrandingId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const sessionTransferToken = pgTable("sessionTransferToken", {
|
||||
token: varchar("token").primaryKey(),
|
||||
sessionId: varchar("sessionId")
|
||||
@@ -215,42 +238,56 @@ export const sessionTransferToken = pgTable("sessionTransferToken", {
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||
});
|
||||
|
||||
export const actionAuditLog = pgTable("actionAuditLog", {
|
||||
id: serial("id").primaryKey(),
|
||||
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: varchar("actorType", { length: 50 }).notNull(),
|
||||
actor: varchar("actor", { length: 255 }).notNull(),
|
||||
actorId: varchar("actorId", { length: 255 }).notNull(),
|
||||
action: varchar("action", { length: 100 }).notNull(),
|
||||
metadata: text("metadata")
|
||||
}, (table) => ([
|
||||
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||
]));
|
||||
export const actionAuditLog = pgTable(
|
||||
"actionAuditLog",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: varchar("actorType", { length: 50 }).notNull(),
|
||||
actor: varchar("actor", { length: 255 }).notNull(),
|
||||
actorId: varchar("actorId", { length: 255 }).notNull(),
|
||||
action: varchar("action", { length: 100 }).notNull(),
|
||||
metadata: text("metadata")
|
||||
},
|
||||
(table) => [
|
||||
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_actionAuditLog_org_timestamp").on(
|
||||
table.orgId,
|
||||
table.timestamp
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export const accessAuditLog = pgTable("accessAuditLog", {
|
||||
id: serial("id").primaryKey(),
|
||||
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: varchar("actorType", { length: 50 }),
|
||||
actor: varchar("actor", { length: 255 }),
|
||||
actorId: varchar("actorId", { length: 255 }),
|
||||
resourceId: integer("resourceId"),
|
||||
ip: varchar("ip", { length: 45 }),
|
||||
type: varchar("type", { length: 100 }).notNull(),
|
||||
action: boolean("action").notNull(),
|
||||
location: text("location"),
|
||||
userAgent: text("userAgent"),
|
||||
metadata: text("metadata")
|
||||
}, (table) => ([
|
||||
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||
]));
|
||||
export const accessAuditLog = pgTable(
|
||||
"accessAuditLog",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||
orgId: varchar("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: varchar("actorType", { length: 50 }),
|
||||
actor: varchar("actor", { length: 255 }),
|
||||
actorId: varchar("actorId", { length: 255 }),
|
||||
resourceId: integer("resourceId"),
|
||||
ip: varchar("ip", { length: 45 }),
|
||||
type: varchar("type", { length: 100 }).notNull(),
|
||||
action: boolean("action").notNull(),
|
||||
location: text("location"),
|
||||
userAgent: text("userAgent"),
|
||||
metadata: text("metadata")
|
||||
},
|
||||
(table) => [
|
||||
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_identityAuditLog_org_timestamp").on(
|
||||
table.orgId,
|
||||
table.timestamp
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
@@ -269,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel<
|
||||
>;
|
||||
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
||||
export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
||||
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
bigint,
|
||||
real,
|
||||
text,
|
||||
index
|
||||
index,
|
||||
uniqueIndex
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { randomUUID } from "crypto";
|
||||
@@ -177,7 +178,7 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
||||
hcMethod: varchar("hcMethod").default("GET"),
|
||||
hcStatus: integer("hcStatus"), // http code
|
||||
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||
hcTlsServerName: text("hcTlsServerName"),
|
||||
hcTlsServerName: text("hcTlsServerName")
|
||||
});
|
||||
|
||||
export const exitNodes = pgTable("exitNodes", {
|
||||
@@ -213,7 +214,10 @@ export const siteResources = pgTable("siteResources", {
|
||||
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
||||
enabled: boolean("enabled").notNull().default(true),
|
||||
alias: varchar("alias"),
|
||||
aliasAddress: varchar("aliasAddress")
|
||||
aliasAddress: varchar("aliasAddress"),
|
||||
tcpPortRangeString: varchar("tcpPortRangeString"),
|
||||
udpPortRangeString: varchar("udpPortRangeString"),
|
||||
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
||||
});
|
||||
|
||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||
|
||||
@@ -52,10 +52,7 @@ export async function getResourceByDomain(
|
||||
resourceHeaderAuth,
|
||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||
)
|
||||
.innerJoin(
|
||||
orgs,
|
||||
eq(orgs.orgId, resources.orgId)
|
||||
)
|
||||
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||
.where(eq(resources.fullDomain, domain))
|
||||
.limit(1);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ function createDb() {
|
||||
|
||||
export const db = createDb();
|
||||
export default db;
|
||||
export const primaryDb = db;
|
||||
export type Transaction = Parameters<
|
||||
Parameters<(typeof db)["transaction"]>[0]
|
||||
>[0];
|
||||
|
||||
@@ -8,7 +8,7 @@ const runMigrations = async () => {
|
||||
console.log("Running migrations...");
|
||||
try {
|
||||
migrate(db as any, {
|
||||
migrationsFolder: migrationsFolder,
|
||||
migrationsFolder: migrationsFolder
|
||||
});
|
||||
console.log("Migrations completed successfully.");
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
sqliteTable,
|
||||
integer,
|
||||
text,
|
||||
real,
|
||||
index
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
||||
import { metadata } from "@app/app/[orgId]/settings/layout";
|
||||
import {
|
||||
index,
|
||||
integer,
|
||||
real,
|
||||
sqliteTable,
|
||||
text
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import { domains, exitNodes, orgs, sessions, users } from "./schema";
|
||||
|
||||
export const certificates = sqliteTable("certificates", {
|
||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||
@@ -29,7 +28,9 @@ export const certificates = sqliteTable("certificates", {
|
||||
});
|
||||
|
||||
export const dnsChallenge = sqliteTable("dnsChallenges", {
|
||||
dnsChallengeId: integer("dnsChallengeId").primaryKey({ autoIncrement: true }),
|
||||
dnsChallengeId: integer("dnsChallengeId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
domain: text("domain").notNull(),
|
||||
token: text("token").notNull(),
|
||||
keyAuthorization: text("keyAuthorization").notNull(),
|
||||
@@ -61,9 +62,7 @@ export const customers = sqliteTable("customers", {
|
||||
});
|
||||
|
||||
export const subscriptions = sqliteTable("subscriptions", {
|
||||
subscriptionId: text("subscriptionId")
|
||||
.primaryKey()
|
||||
.notNull(),
|
||||
subscriptionId: text("subscriptionId").primaryKey().notNull(),
|
||||
customerId: text("customerId")
|
||||
.notNull()
|
||||
.references(() => customers.customerId, { onDelete: "cascade" }),
|
||||
@@ -75,7 +74,9 @@ export const subscriptions = sqliteTable("subscriptions", {
|
||||
});
|
||||
|
||||
export const subscriptionItems = sqliteTable("subscriptionItems", {
|
||||
subscriptionItemId: integer("subscriptionItemId").primaryKey({ autoIncrement: true }),
|
||||
subscriptionItemId: integer("subscriptionItemId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
subscriptionId: text("subscriptionId")
|
||||
.notNull()
|
||||
.references(() => subscriptions.subscriptionId, {
|
||||
@@ -129,7 +130,9 @@ export const limits = sqliteTable("limits", {
|
||||
});
|
||||
|
||||
export const usageNotifications = sqliteTable("usageNotifications", {
|
||||
notificationId: integer("notificationId").primaryKey({ autoIncrement: true }),
|
||||
notificationId: integer("notificationId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
@@ -199,6 +202,31 @@ export const loginPageOrg = sqliteTable("loginPageOrg", {
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const loginPageBranding = sqliteTable("loginPageBranding", {
|
||||
loginPageBrandingId: integer("loginPageBrandingId").primaryKey({
|
||||
autoIncrement: true
|
||||
}),
|
||||
logoUrl: text("logoUrl").notNull(),
|
||||
logoWidth: integer("logoWidth").notNull(),
|
||||
logoHeight: integer("logoHeight").notNull(),
|
||||
primaryColor: text("primaryColor"),
|
||||
resourceTitle: text("resourceTitle").notNull(),
|
||||
resourceSubtitle: text("resourceSubtitle"),
|
||||
orgTitle: text("orgTitle"),
|
||||
orgSubtitle: text("orgSubtitle")
|
||||
});
|
||||
|
||||
export const loginPageBrandingOrg = sqliteTable("loginPageBrandingOrg", {
|
||||
loginPageBrandingId: integer("loginPageBrandingId")
|
||||
.notNull()
|
||||
.references(() => loginPageBranding.loginPageBrandingId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||
});
|
||||
|
||||
export const sessionTransferToken = sqliteTable("sessionTransferToken", {
|
||||
token: text("token").primaryKey(),
|
||||
sessionId: text("sessionId")
|
||||
@@ -210,42 +238,56 @@ export const sessionTransferToken = sqliteTable("sessionTransferToken", {
|
||||
expiresAt: integer("expiresAt").notNull()
|
||||
});
|
||||
|
||||
export const actionAuditLog = sqliteTable("actionAuditLog", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: text("actorType").notNull(),
|
||||
actor: text("actor").notNull(),
|
||||
actorId: text("actorId").notNull(),
|
||||
action: text("action").notNull(),
|
||||
metadata: text("metadata")
|
||||
}, (table) => ([
|
||||
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||
]));
|
||||
export const actionAuditLog = sqliteTable(
|
||||
"actionAuditLog",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: text("actorType").notNull(),
|
||||
actor: text("actor").notNull(),
|
||||
actorId: text("actorId").notNull(),
|
||||
action: text("action").notNull(),
|
||||
metadata: text("metadata")
|
||||
},
|
||||
(table) => [
|
||||
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_actionAuditLog_org_timestamp").on(
|
||||
table.orgId,
|
||||
table.timestamp
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export const accessAuditLog = sqliteTable("accessAuditLog", {
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: text("actorType"),
|
||||
actor: text("actor"),
|
||||
actorId: text("actorId"),
|
||||
resourceId: integer("resourceId"),
|
||||
ip: text("ip"),
|
||||
location: text("location"),
|
||||
type: text("type").notNull(),
|
||||
action: integer("action", { mode: "boolean" }).notNull(),
|
||||
userAgent: text("userAgent"),
|
||||
metadata: text("metadata")
|
||||
}, (table) => ([
|
||||
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
||||
]));
|
||||
export const accessAuditLog = sqliteTable(
|
||||
"accessAuditLog",
|
||||
{
|
||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||
orgId: text("orgId")
|
||||
.notNull()
|
||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||
actorType: text("actorType"),
|
||||
actor: text("actor"),
|
||||
actorId: text("actorId"),
|
||||
resourceId: integer("resourceId"),
|
||||
ip: text("ip"),
|
||||
location: text("location"),
|
||||
type: text("type").notNull(),
|
||||
action: integer("action", { mode: "boolean" }).notNull(),
|
||||
userAgent: text("userAgent"),
|
||||
metadata: text("metadata")
|
||||
},
|
||||
(table) => [
|
||||
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
||||
index("idx_identityAuditLog_org_timestamp").on(
|
||||
table.orgId,
|
||||
table.timestamp
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
@@ -264,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel<
|
||||
>;
|
||||
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
||||
export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
||||
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { randomUUID } from "crypto";
|
||||
import { InferSelectModel } from "drizzle-orm";
|
||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||
import {
|
||||
sqliteTable,
|
||||
text,
|
||||
integer,
|
||||
index,
|
||||
uniqueIndex
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
import { no } from "zod/v4/locales";
|
||||
|
||||
export const domains = sqliteTable("domains", {
|
||||
@@ -234,7 +240,10 @@ export const siteResources = sqliteTable("siteResources", {
|
||||
destination: text("destination").notNull(), // ip, cidr, hostname
|
||||
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||
alias: text("alias"),
|
||||
aliasAddress: text("aliasAddress")
|
||||
aliasAddress: text("aliasAddress"),
|
||||
tcpPortRangeString: text("tcpPortRangeString"),
|
||||
udpPortRangeString: text("udpPortRangeString"),
|
||||
disableIcmp: integer("disableIcmp", { mode: "boolean" })
|
||||
});
|
||||
|
||||
export const clientSiteResources = sqliteTable("clientSiteResources", {
|
||||
|
||||
@@ -18,10 +18,13 @@ function createEmailClient() {
|
||||
host: emailConfig.smtp_host,
|
||||
port: emailConfig.smtp_port,
|
||||
secure: emailConfig.smtp_secure || false,
|
||||
auth: (emailConfig.smtp_user && emailConfig.smtp_pass) ? {
|
||||
user: emailConfig.smtp_user,
|
||||
pass: emailConfig.smtp_pass
|
||||
} : null
|
||||
auth:
|
||||
emailConfig.smtp_user && emailConfig.smtp_pass
|
||||
? {
|
||||
user: emailConfig.smtp_user,
|
||||
pass: emailConfig.smtp_pass
|
||||
}
|
||||
: null
|
||||
} as SMTPTransport.Options;
|
||||
|
||||
if (emailConfig.smtp_tls_reject_unauthorized !== undefined) {
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function sendEmail(
|
||||
from: string | undefined;
|
||||
to: string | undefined;
|
||||
subject: string;
|
||||
replyTo?: string;
|
||||
}
|
||||
) {
|
||||
if (!emailClient) {
|
||||
@@ -32,6 +33,7 @@ export async function sendEmail(
|
||||
address: opts.from
|
||||
},
|
||||
to: opts.to,
|
||||
replyTo: opts.replyTo,
|
||||
subject: opts.subject,
|
||||
html: emailHtml
|
||||
});
|
||||
|
||||
@@ -19,7 +19,13 @@ interface Props {
|
||||
billingLink: string; // Link to billing page
|
||||
}
|
||||
|
||||
export const NotifyUsageLimitApproaching = ({ email, limitName, currentUsage, usageLimit, billingLink }: Props) => {
|
||||
export const NotifyUsageLimitApproaching = ({
|
||||
email,
|
||||
limitName,
|
||||
currentUsage,
|
||||
usageLimit,
|
||||
billingLink
|
||||
}: Props) => {
|
||||
const previewText = `Your usage for ${limitName} is approaching the limit.`;
|
||||
const usagePercentage = Math.round((currentUsage / usageLimit) * 100);
|
||||
|
||||
@@ -37,23 +43,32 @@ export const NotifyUsageLimitApproaching = ({ email, limitName, currentUsage, us
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
We wanted to let you know that your usage for <strong>{limitName}</strong> is approaching your plan limit.
|
||||
We wanted to let you know that your usage for{" "}
|
||||
<strong>{limitName}</strong> is approaching your
|
||||
plan limit.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
<strong>Current Usage:</strong> {currentUsage} of {usageLimit} ({usagePercentage}%)
|
||||
<strong>Current Usage:</strong> {currentUsage} of{" "}
|
||||
{usageLimit} ({usagePercentage}%)
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
Once you reach your limit, some functionality may be restricted or your sites may disconnect until you upgrade your plan or your usage resets.
|
||||
Once you reach your limit, some functionality may be
|
||||
restricted or your sites may disconnect until you
|
||||
upgrade your plan or your usage resets.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
To avoid any interruption to your service, we recommend upgrading your plan or monitoring your usage closely. You can <a href={billingLink}>upgrade your plan here</a>.
|
||||
To avoid any interruption to your service, we
|
||||
recommend upgrading your plan or monitoring your
|
||||
usage closely. You can{" "}
|
||||
<a href={billingLink}>upgrade your plan here</a>.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
If you have any questions or need assistance, please don't hesitate to reach out to our support team.
|
||||
If you have any questions or need assistance, please
|
||||
don't hesitate to reach out to our support team.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
|
||||
@@ -19,7 +19,13 @@ interface Props {
|
||||
billingLink: string; // Link to billing page
|
||||
}
|
||||
|
||||
export const NotifyUsageLimitReached = ({ email, limitName, currentUsage, usageLimit, billingLink }: Props) => {
|
||||
export const NotifyUsageLimitReached = ({
|
||||
email,
|
||||
limitName,
|
||||
currentUsage,
|
||||
usageLimit,
|
||||
billingLink
|
||||
}: Props) => {
|
||||
const previewText = `You've reached your ${limitName} usage limit - Action required`;
|
||||
const usagePercentage = Math.round((currentUsage / usageLimit) * 100);
|
||||
|
||||
@@ -32,30 +38,48 @@ export const NotifyUsageLimitReached = ({ email, limitName, currentUsage, usageL
|
||||
<EmailContainer>
|
||||
<EmailLetterHead />
|
||||
|
||||
<EmailHeading>Usage Limit Reached - Action Required</EmailHeading>
|
||||
<EmailHeading>
|
||||
Usage Limit Reached - Action Required
|
||||
</EmailHeading>
|
||||
|
||||
<EmailGreeting>Hi there,</EmailGreeting>
|
||||
|
||||
<EmailText>
|
||||
You have reached your usage limit for <strong>{limitName}</strong>.
|
||||
You have reached your usage limit for{" "}
|
||||
<strong>{limitName}</strong>.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
<strong>Current Usage:</strong> {currentUsage} of {usageLimit} ({usagePercentage}%)
|
||||
<strong>Current Usage:</strong> {currentUsage} of{" "}
|
||||
{usageLimit} ({usagePercentage}%)
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
<strong>Important:</strong> Your functionality may now be restricted and your sites may disconnect until you either upgrade your plan or your usage resets. To prevent any service interruption, immediate action is recommended.
|
||||
<strong>Important:</strong> Your functionality may
|
||||
now be restricted and your sites may disconnect
|
||||
until you either upgrade your plan or your usage
|
||||
resets. To prevent any service interruption,
|
||||
immediate action is recommended.
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
<strong>What you can do:</strong>
|
||||
<br />• <a href={billingLink} style={{ color: '#2563eb', fontWeight: 'bold' }}>Upgrade your plan immediately</a> to restore full functionality
|
||||
<br />• Monitor your usage to stay within limits in the future
|
||||
<br />•{" "}
|
||||
<a
|
||||
href={billingLink}
|
||||
style={{ color: "#2563eb", fontWeight: "bold" }}
|
||||
>
|
||||
Upgrade your plan immediately
|
||||
</a>{" "}
|
||||
to restore full functionality
|
||||
<br />• Monitor your usage to stay within limits in
|
||||
the future
|
||||
</EmailText>
|
||||
|
||||
<EmailText>
|
||||
If you have any questions or need immediate assistance, please contact our support team right away.
|
||||
If you have any questions or need immediate
|
||||
assistance, please contact our support team right
|
||||
away.
|
||||
</EmailText>
|
||||
|
||||
<EmailFooter>
|
||||
|
||||
@@ -5,7 +5,7 @@ import config from "@server/lib/config";
|
||||
import logger from "@server/logger";
|
||||
import {
|
||||
errorHandlerMiddleware,
|
||||
notFoundMiddleware,
|
||||
notFoundMiddleware
|
||||
} from "@server/middlewares";
|
||||
import { authenticated, unauthenticated } from "#dynamic/routers/integration";
|
||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||
|
||||
29
server/lib/asn.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import logger from "@server/logger";
|
||||
import { maxmindAsnLookup } from "@server/db/maxmindAsn";
|
||||
|
||||
export async function getAsnForIp(ip: string): Promise<number | undefined> {
|
||||
try {
|
||||
if (!maxmindAsnLookup) {
|
||||
logger.debug(
|
||||
"MaxMind ASN DB path not configured, cannot perform ASN lookup"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = maxmindAsnLookup.get(ip);
|
||||
|
||||
if (!result || !result.autonomous_system_number) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`ASN lookup successful for IP ${ip}: AS${result.autonomous_system_number}`
|
||||
);
|
||||
|
||||
return result.autonomous_system_number;
|
||||
} catch (error) {
|
||||
logger.error("Error performing ASN lookup:", error);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -25,16 +25,22 @@ export const FeatureMeterIdsSandbox: Record<FeatureId, string> = {
|
||||
};
|
||||
|
||||
export function getFeatureMeterId(featureId: FeatureId): string {
|
||||
if (process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
) {
|
||||
return FeatureMeterIds[featureId];
|
||||
} else {
|
||||
return FeatureMeterIdsSandbox[featureId];
|
||||
}
|
||||
}
|
||||
|
||||
export function getFeatureIdByMetricId(metricId: string): FeatureId | undefined {
|
||||
return (Object.entries(FeatureMeterIds) as [FeatureId, string][])
|
||||
.find(([_, v]) => v === metricId)?.[0];
|
||||
export function getFeatureIdByMetricId(
|
||||
metricId: string
|
||||
): FeatureId | undefined {
|
||||
return (Object.entries(FeatureMeterIds) as [FeatureId, string][]).find(
|
||||
([_, v]) => v === metricId
|
||||
)?.[0];
|
||||
}
|
||||
|
||||
export type FeaturePriceSet = {
|
||||
@@ -43,7 +49,8 @@ export type FeaturePriceSet = {
|
||||
[FeatureId.DOMAINS]?: string; // Optional since domains are not billed
|
||||
};
|
||||
|
||||
export const standardFeaturePriceSet: FeaturePriceSet = { // Free tier matches the freeLimitSet
|
||||
export const standardFeaturePriceSet: FeaturePriceSet = {
|
||||
// Free tier matches the freeLimitSet
|
||||
[FeatureId.SITE_UPTIME]: "price_1RrQc4D3Ee2Ir7WmaJGZ3MtF",
|
||||
[FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea",
|
||||
[FeatureId.EGRESS_DATA_MB]: "price_1RrQXFD3Ee2Ir7WmvGDlgxQk",
|
||||
@@ -51,7 +58,8 @@ export const standardFeaturePriceSet: FeaturePriceSet = { // Free tier matches t
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "price_1S46weD3Ee2Ir7Wm94KEHI4h"
|
||||
};
|
||||
|
||||
export const standardFeaturePriceSetSandbox: FeaturePriceSet = { // Free tier matches the freeLimitSet
|
||||
export const standardFeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
// Free tier matches the freeLimitSet
|
||||
[FeatureId.SITE_UPTIME]: "price_1RefFBDCpkOb237BPrKZ8IEU",
|
||||
[FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF",
|
||||
[FeatureId.EGRESS_DATA_MB]: "price_1Rfp9LDCpkOb237BwuN5Oiu0",
|
||||
@@ -60,15 +68,20 @@ export const standardFeaturePriceSetSandbox: FeaturePriceSet = { // Free tier ma
|
||||
};
|
||||
|
||||
export function getStandardFeaturePriceSet(): FeaturePriceSet {
|
||||
if (process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
) {
|
||||
return standardFeaturePriceSet;
|
||||
} else {
|
||||
return standardFeaturePriceSetSandbox;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLineItems(featurePriceSet: FeaturePriceSet): Stripe.Checkout.SessionCreateParams.LineItem[] {
|
||||
export function getLineItems(
|
||||
featurePriceSet: FeaturePriceSet
|
||||
): Stripe.Checkout.SessionCreateParams.LineItem[] {
|
||||
return Object.entries(featurePriceSet).map(([featureId, priceId]) => ({
|
||||
price: priceId,
|
||||
price: priceId
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ export * from "./limitSet";
|
||||
export * from "./features";
|
||||
export * from "./limitsService";
|
||||
export * from "./getOrgTierData";
|
||||
export * from "./createCustomer";
|
||||
export * from "./createCustomer";
|
||||
|
||||
@@ -12,7 +12,7 @@ export const sandboxLimitSet: LimitSet = {
|
||||
[FeatureId.USERS]: { value: 1, description: "Sandbox limit" },
|
||||
[FeatureId.EGRESS_DATA_MB]: { value: 1000, description: "Sandbox limit" }, // 1 GB
|
||||
[FeatureId.DOMAINS]: { value: 0, description: "Sandbox limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" },
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 0, description: "Sandbox limit" }
|
||||
};
|
||||
|
||||
export const freeLimitSet: LimitSet = {
|
||||
@@ -29,7 +29,7 @@ export const freeLimitSet: LimitSet = {
|
||||
export const subscribedLimitSet: LimitSet = {
|
||||
[FeatureId.SITE_UPTIME]: {
|
||||
value: 2232000,
|
||||
description: "Contact us to increase soft limit.",
|
||||
description: "Contact us to increase soft limit."
|
||||
}, // 50 sites up for 31 days
|
||||
[FeatureId.USERS]: {
|
||||
value: 150,
|
||||
@@ -38,7 +38,7 @@ export const subscribedLimitSet: LimitSet = {
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 12000000,
|
||||
description: "Contact us to increase soft limit."
|
||||
}, // 12000 GB
|
||||
}, // 12000 GB
|
||||
[FeatureId.DOMAINS]: {
|
||||
value: 25,
|
||||
description: "Contact us to increase soft limit."
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
export enum TierId {
|
||||
STANDARD = "standard",
|
||||
STANDARD = "standard"
|
||||
}
|
||||
|
||||
export type TierPriceSet = {
|
||||
[key in TierId]: string;
|
||||
};
|
||||
|
||||
export const tierPriceSet: TierPriceSet = { // Free tier matches the freeLimitSet
|
||||
[TierId.STANDARD]: "price_1RrQ9cD3Ee2Ir7Wmqdy3KBa0",
|
||||
export const tierPriceSet: TierPriceSet = {
|
||||
// Free tier matches the freeLimitSet
|
||||
[TierId.STANDARD]: "price_1RrQ9cD3Ee2Ir7Wmqdy3KBa0"
|
||||
};
|
||||
|
||||
export const tierPriceSetSandbox: TierPriceSet = { // Free tier matches the freeLimitSet
|
||||
export const tierPriceSetSandbox: TierPriceSet = {
|
||||
// Free tier matches the freeLimitSet
|
||||
// when matching tier the keys closer to 0 index are matched first so list the tiers in descending order of value
|
||||
[TierId.STANDARD]: "price_1RrAYJDCpkOb237By2s1P32m",
|
||||
[TierId.STANDARD]: "price_1RrAYJDCpkOb237By2s1P32m"
|
||||
};
|
||||
|
||||
export function getTierPriceSet(environment?: string, sandbox_mode?: boolean): TierPriceSet {
|
||||
if ((process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true") || (environment === "prod" && sandbox_mode !== true)) { // THIS GETS LOADED CLIENT SIDE AND SERVER SIDE
|
||||
export function getTierPriceSet(
|
||||
environment?: string,
|
||||
sandbox_mode?: boolean
|
||||
): TierPriceSet {
|
||||
if (
|
||||
(process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true") ||
|
||||
(environment === "prod" && sandbox_mode !== true)
|
||||
) {
|
||||
// THIS GETS LOADED CLIENT SIDE AND SERVER SIDE
|
||||
return tierPriceSet;
|
||||
} else {
|
||||
return tierPriceSetSandbox;
|
||||
|
||||
@@ -19,7 +19,7 @@ import logger from "@server/logger";
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { build } from "@server/build";
|
||||
import { s3Client } from "@server/lib/s3";
|
||||
import cache from "@server/lib/cache";
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
interface StripeEvent {
|
||||
identifier?: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, newts, blueprints, Blueprint } from "@server/db";
|
||||
import { db, newts, blueprints, Blueprint, Site, siteResources, roleSiteResources, userSiteResources, clientSiteResources } from "@server/db";
|
||||
import { Config, ConfigSchema } from "./types";
|
||||
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
|
||||
import { fromError } from "zod-validation-error";
|
||||
@@ -15,6 +15,7 @@ import { BlueprintSource } from "@server/routers/blueprints/types";
|
||||
import { stringify as stringifyYaml } from "yaml";
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations";
|
||||
|
||||
type ApplyBlueprintArgs = {
|
||||
orgId: string;
|
||||
@@ -108,38 +109,136 @@ export async function applyBlueprint({
|
||||
|
||||
// We need to update the targets on the newts from the successfully updated information
|
||||
for (const result of clientResourcesResults) {
|
||||
const [site] = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(sites.siteId, result.newSiteResource.siteId),
|
||||
eq(sites.orgId, orgId),
|
||||
eq(sites.type, "newt"),
|
||||
isNotNull(sites.pubKey)
|
||||
if (
|
||||
result.oldSiteResource &&
|
||||
result.oldSiteResource.siteId !=
|
||||
result.newSiteResource.siteId
|
||||
) {
|
||||
// query existing associations
|
||||
const existingRoleIds = await trx
|
||||
.select()
|
||||
.from(roleSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
roleSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
.then((rows) => rows.map((row) => row.roleId));
|
||||
|
||||
if (!site) {
|
||||
logger.debug(
|
||||
`No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
|
||||
const existingUserIds= await trx
|
||||
.select()
|
||||
.from(userSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
userSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
).then((rows) => rows.map((row) => row.userId));
|
||||
|
||||
const existingClientIds = await trx
|
||||
.select()
|
||||
.from(clientSiteResources)
|
||||
.where(
|
||||
eq(
|
||||
clientSiteResources.siteResourceId,
|
||||
result.oldSiteResource.siteResourceId
|
||||
)
|
||||
).then((rows) => rows.map((row) => row.clientId));
|
||||
|
||||
// delete the existing site resource
|
||||
await trx
|
||||
.delete(siteResources)
|
||||
.where(
|
||||
and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId))
|
||||
);
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
result.oldSiteResource,
|
||||
trx
|
||||
);
|
||||
|
||||
const [insertedSiteResource] = await trx
|
||||
.insert(siteResources)
|
||||
.values({
|
||||
...result.newSiteResource,
|
||||
})
|
||||
.returning();
|
||||
|
||||
// wait some time to allow for messages to be handled
|
||||
await new Promise((resolve) => setTimeout(resolve, 750));
|
||||
|
||||
//////////////////// update the associations ////////////////////
|
||||
|
||||
if (existingRoleIds.length > 0) {
|
||||
await trx.insert(roleSiteResources).values(
|
||||
existingRoleIds.map((roleId) => ({
|
||||
roleId,
|
||||
siteResourceId: insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUserIds.length > 0) {
|
||||
await trx.insert(userSiteResources).values(
|
||||
existingUserIds.map((userId) => ({
|
||||
userId,
|
||||
siteResourceId: insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (existingClientIds.length > 0) {
|
||||
await trx.insert(clientSiteResources).values(
|
||||
existingClientIds.map((clientId) => ({
|
||||
clientId,
|
||||
siteResourceId: insertedSiteResource!.siteResourceId
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
await rebuildClientAssociationsFromSiteResource(
|
||||
insertedSiteResource,
|
||||
trx
|
||||
);
|
||||
|
||||
} else {
|
||||
const [newSite] = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.innerJoin(newts, eq(sites.siteId, newts.siteId))
|
||||
.where(
|
||||
and(
|
||||
eq(sites.siteId, result.newSiteResource.siteId),
|
||||
eq(sites.orgId, orgId),
|
||||
eq(sites.type, "newt"),
|
||||
isNotNull(sites.pubKey)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!newSite) {
|
||||
logger.debug(
|
||||
`No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.sites.siteId}`
|
||||
);
|
||||
|
||||
await handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
{
|
||||
siteId: newSite.sites.siteId,
|
||||
orgId: newSite.sites.orgId
|
||||
},
|
||||
trx
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Updating client resource ${result.newSiteResource.siteResourceId} on site ${site.sites.siteId}`
|
||||
);
|
||||
|
||||
await handleMessagingForUpdatedSiteResource(
|
||||
result.oldSiteResource,
|
||||
result.newSiteResource,
|
||||
{ siteId: site.sites.siteId, orgId: site.sites.orgId },
|
||||
trx
|
||||
);
|
||||
|
||||
// await addClientTargets(
|
||||
// site.newt.newtId,
|
||||
// result.resource.destination,
|
||||
|
||||
@@ -34,7 +34,10 @@ export async function applyNewtDockerBlueprint(
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEmptyObject(blueprint["proxy-resources"]) && isEmptyObject(blueprint["client-resources"])) {
|
||||
if (
|
||||
isEmptyObject(blueprint["proxy-resources"]) &&
|
||||
isEmptyObject(blueprint["client-resources"])
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { sites } from "@server/db";
|
||||
import { eq, and, ne, inArray } from "drizzle-orm";
|
||||
import { Config } from "./types";
|
||||
import logger from "@server/logger";
|
||||
import { getNextAvailableAliasAddress } from "../ip";
|
||||
|
||||
export type ClientResourcesResults = {
|
||||
newSiteResource: SiteResource;
|
||||
@@ -75,22 +76,20 @@ export async function updateClientResources(
|
||||
}
|
||||
|
||||
if (existingResource) {
|
||||
if (existingResource.siteId !== site.siteId) {
|
||||
throw new Error(
|
||||
`You can not change the site of an existing client resource (${resourceNiceId}). Please delete and recreate it instead.`
|
||||
);
|
||||
}
|
||||
|
||||
// Update existing resource
|
||||
const [updatedResource] = await trx
|
||||
.update(siteResources)
|
||||
.set({
|
||||
name: resourceData.name || resourceNiceId,
|
||||
siteId: site.siteId,
|
||||
mode: resourceData.mode,
|
||||
destination: resourceData.destination,
|
||||
enabled: true, // hardcoded for now
|
||||
// enabled: resourceData.enabled ?? true,
|
||||
alias: resourceData.alias || null
|
||||
alias: resourceData.alias || null,
|
||||
disableIcmp: resourceData["disable-icmp"],
|
||||
tcpPortRangeString: resourceData["tcp-ports"],
|
||||
udpPortRangeString: resourceData["udp-ports"]
|
||||
})
|
||||
.where(
|
||||
eq(
|
||||
@@ -205,6 +204,12 @@ export async function updateClientResources(
|
||||
oldSiteResource: existingResource
|
||||
});
|
||||
} else {
|
||||
let aliasAddress: string | null = null;
|
||||
if (resourceData.mode == "host") {
|
||||
// we can only have an alias on a host
|
||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||
}
|
||||
|
||||
// Create new resource
|
||||
const [newResource] = await trx
|
||||
.insert(siteResources)
|
||||
@@ -217,7 +222,11 @@ export async function updateClientResources(
|
||||
destination: resourceData.destination,
|
||||
enabled: true, // hardcoded for now
|
||||
// enabled: resourceData.enabled ?? true,
|
||||
alias: resourceData.alias || null
|
||||
alias: resourceData.alias || null,
|
||||
aliasAddress: aliasAddress,
|
||||
disableIcmp: resourceData["disable-icmp"],
|
||||
tcpPortRangeString: resourceData["tcp-ports"],
|
||||
udpPortRangeString: resourceData["udp-ports"]
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
@@ -84,12 +84,20 @@ export function processContainerLabels(containers: Container[]): {
|
||||
|
||||
// Process proxy resources
|
||||
if (Object.keys(proxyResourceLabels).length > 0) {
|
||||
processResourceLabels(proxyResourceLabels, container, result["proxy-resources"]);
|
||||
processResourceLabels(
|
||||
proxyResourceLabels,
|
||||
container,
|
||||
result["proxy-resources"]
|
||||
);
|
||||
}
|
||||
|
||||
// Process client resources
|
||||
if (Object.keys(clientResourceLabels).length > 0) {
|
||||
processResourceLabels(clientResourceLabels, container, result["client-resources"]);
|
||||
processResourceLabels(
|
||||
clientResourceLabels,
|
||||
container,
|
||||
result["client-resources"]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -161,8 +169,7 @@ function processResourceLabels(
|
||||
const finalTarget = { ...target };
|
||||
if (!finalTarget.hostname) {
|
||||
finalTarget.hostname =
|
||||
container.name ||
|
||||
container.hostname;
|
||||
container.name || container.hostname;
|
||||
}
|
||||
if (!finalTarget.port) {
|
||||
const containerPort =
|
||||
|
||||
@@ -1086,10 +1086,8 @@ async function getDomainId(
|
||||
|
||||
// remove the base domain of the domain
|
||||
let subdomain = null;
|
||||
if (domainSelection.type == "ns" || domainSelection.type == "wildcard") {
|
||||
if (fullDomain != baseDomain) {
|
||||
subdomain = fullDomain.replace(`.${baseDomain}`, "");
|
||||
}
|
||||
if (fullDomain != baseDomain) {
|
||||
subdomain = fullDomain.replace(`.${baseDomain}`, "");
|
||||
}
|
||||
|
||||
// Return the first valid domain
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { portRangeStringSchema } from "@server/lib/ip";
|
||||
|
||||
export const SiteSchema = z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
@@ -71,11 +72,71 @@ export const AuthSchema = z.object({
|
||||
"auto-login-idp": z.int().positive().optional()
|
||||
});
|
||||
|
||||
export const RuleSchema = z.object({
|
||||
action: z.enum(["allow", "deny", "pass"]),
|
||||
match: z.enum(["cidr", "path", "ip", "country"]),
|
||||
value: z.string()
|
||||
});
|
||||
export const RuleSchema = z
|
||||
.object({
|
||||
action: z.enum(["allow", "deny", "pass"]),
|
||||
match: z.enum(["cidr", "path", "ip", "country", "asn"]),
|
||||
value: z.string()
|
||||
})
|
||||
.refine(
|
||||
(rule) => {
|
||||
if (rule.match === "ip") {
|
||||
// Check if it's a valid IP address (v4 or v6)
|
||||
return z.union([z.ipv4(), z.ipv6()]).safeParse(rule.value)
|
||||
.success;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
path: ["value"],
|
||||
message: "Value must be a valid IP address when match is 'ip'"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(rule) => {
|
||||
if (rule.match === "cidr") {
|
||||
// Check if it's a valid CIDR (v4 or v6)
|
||||
return z.union([z.cidrv4(), z.cidrv6()]).safeParse(rule.value)
|
||||
.success;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
path: ["value"],
|
||||
message: "Value must be a valid CIDR notation when match is 'cidr'"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(rule) => {
|
||||
if (rule.match === "country") {
|
||||
// Check if it's a valid 2-letter country code
|
||||
return /^[A-Z]{2}$/.test(rule.value);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
path: ["value"],
|
||||
message:
|
||||
"Value must be a 2-letter country code when match is 'country'"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(rule) => {
|
||||
if (rule.match === "asn") {
|
||||
// Check if it's either AS<number> format or just a number
|
||||
const asNumberPattern = /^AS\d+$/i;
|
||||
const isASFormat = asNumberPattern.test(rule.value);
|
||||
const isNumeric = /^\d+$/.test(rule.value);
|
||||
return isASFormat || isNumeric;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
path: ["value"],
|
||||
message:
|
||||
"Value must be either 'AS<number>' format or a number when match is 'asn'"
|
||||
}
|
||||
);
|
||||
|
||||
export const HeaderSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
@@ -222,6 +283,9 @@ export const ClientResourceSchema = z
|
||||
// destinationPort: z.int().positive().optional(),
|
||||
destination: z.string().min(1),
|
||||
// enabled: z.boolean().default(true),
|
||||
"tcp-ports": portRangeStringSchema.optional().default("*"),
|
||||
"udp-ports": portRangeStringSchema.optional().default("*"),
|
||||
"disable-icmp": z.boolean().optional().default(false),
|
||||
alias: z
|
||||
.string()
|
||||
.regex(
|
||||
@@ -312,7 +376,7 @@ export const ConfigSchema = z
|
||||
};
|
||||
delete (data as any)["public-resources"];
|
||||
}
|
||||
|
||||
|
||||
// Merge private-resources into client-resources
|
||||
if (data["private-resources"]) {
|
||||
data["client-resources"] = {
|
||||
@@ -321,10 +385,13 @@ export const ConfigSchema = z
|
||||
};
|
||||
delete (data as any)["private-resources"];
|
||||
}
|
||||
|
||||
|
||||
return data as {
|
||||
"proxy-resources": Record<string, z.infer<typeof ResourceSchema>>;
|
||||
"client-resources": Record<string, z.infer<typeof ClientResourceSchema>>;
|
||||
"client-resources": Record<
|
||||
string,
|
||||
z.infer<typeof ClientResourceSchema>
|
||||
>;
|
||||
sites: Record<string, z.infer<typeof SiteSchema>>;
|
||||
};
|
||||
})
|
||||
|
||||
@@ -2,4 +2,4 @@ import NodeCache from "node-cache";
|
||||
|
||||
export const cache = new NodeCache({ stdTTL: 3600, checkperiod: 120 });
|
||||
|
||||
export default cache;
|
||||
export default cache;
|
||||
|
||||
@@ -166,7 +166,10 @@ export async function calculateUserClientsForOrgs(
|
||||
];
|
||||
|
||||
// Get next available subnet
|
||||
const newSubnet = await getNextAvailableClientSubnet(orgId);
|
||||
const newSubnet = await getNextAvailableClientSubnet(
|
||||
orgId,
|
||||
transaction
|
||||
);
|
||||
if (!newSubnet) {
|
||||
logger.warn(
|
||||
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no available subnet found`
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
|
||||
export async function getValidCertificatesForDomains(
|
||||
domains: Set<string>
|
||||
): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
@@ -10,4 +12,4 @@ export async function getValidCertificatesForDomains(domains: Set<string>): Prom
|
||||
}>
|
||||
> {
|
||||
return []; // stub
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ function dateToTimestamp(dateStr: string): number {
|
||||
|
||||
// Testable version of calculateCutoffTimestamp that accepts a "now" timestamp
|
||||
// This matches the logic in cleanupLogs.ts but allows injecting the current time
|
||||
function calculateCutoffTimestampWithNow(retentionDays: number, nowTimestamp: number): number {
|
||||
function calculateCutoffTimestampWithNow(
|
||||
retentionDays: number,
|
||||
nowTimestamp: number
|
||||
): number {
|
||||
if (retentionDays === 9001) {
|
||||
// Special case: data is erased at the end of the year following the year it was generated
|
||||
// This means we delete logs from 2 years ago or older (logs from year Y are deleted after Dec 31 of year Y+1)
|
||||
@@ -28,7 +31,7 @@ function testCalculateCutoffTimestamp() {
|
||||
{
|
||||
const now = dateToTimestamp("2025-12-06T12:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(30, now);
|
||||
const expected = now - (30 * 24 * 60 * 60);
|
||||
const expected = now - 30 * 24 * 60 * 60;
|
||||
assertEquals(result, expected, "30 days retention calculation failed");
|
||||
}
|
||||
|
||||
@@ -36,7 +39,7 @@ function testCalculateCutoffTimestamp() {
|
||||
{
|
||||
const now = dateToTimestamp("2025-06-15T00:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(90, now);
|
||||
const expected = now - (90 * 24 * 60 * 60);
|
||||
const expected = now - 90 * 24 * 60 * 60;
|
||||
assertEquals(result, expected, "90 days retention calculation failed");
|
||||
}
|
||||
|
||||
@@ -48,7 +51,11 @@ function testCalculateCutoffTimestamp() {
|
||||
const now = dateToTimestamp("2025-12-06T12:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(9001, now);
|
||||
const expected = dateToTimestamp("2024-01-01T00:00:00Z");
|
||||
assertEquals(result, expected, "9001 retention (Dec 2025) - should cutoff at Jan 1, 2024");
|
||||
assertEquals(
|
||||
result,
|
||||
expected,
|
||||
"9001 retention (Dec 2025) - should cutoff at Jan 1, 2024"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 4: Special case 9001 - January 2026
|
||||
@@ -58,7 +65,11 @@ function testCalculateCutoffTimestamp() {
|
||||
const now = dateToTimestamp("2026-01-15T12:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(9001, now);
|
||||
const expected = dateToTimestamp("2025-01-01T00:00:00Z");
|
||||
assertEquals(result, expected, "9001 retention (Jan 2026) - should cutoff at Jan 1, 2025");
|
||||
assertEquals(
|
||||
result,
|
||||
expected,
|
||||
"9001 retention (Jan 2026) - should cutoff at Jan 1, 2025"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 5: Special case 9001 - December 31, 2025 at 23:59:59 UTC
|
||||
@@ -68,7 +79,11 @@ function testCalculateCutoffTimestamp() {
|
||||
const now = dateToTimestamp("2025-12-31T23:59:59Z");
|
||||
const result = calculateCutoffTimestampWithNow(9001, now);
|
||||
const expected = dateToTimestamp("2024-01-01T00:00:00Z");
|
||||
assertEquals(result, expected, "9001 retention (Dec 31, 2025 23:59:59) - should cutoff at Jan 1, 2024");
|
||||
assertEquals(
|
||||
result,
|
||||
expected,
|
||||
"9001 retention (Dec 31, 2025 23:59:59) - should cutoff at Jan 1, 2024"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 6: Special case 9001 - January 1, 2026 at 00:00:01 UTC
|
||||
@@ -78,7 +93,11 @@ function testCalculateCutoffTimestamp() {
|
||||
const now = dateToTimestamp("2026-01-01T00:00:01Z");
|
||||
const result = calculateCutoffTimestampWithNow(9001, now);
|
||||
const expected = dateToTimestamp("2025-01-01T00:00:00Z");
|
||||
assertEquals(result, expected, "9001 retention (Jan 1, 2026 00:00:01) - should cutoff at Jan 1, 2025");
|
||||
assertEquals(
|
||||
result,
|
||||
expected,
|
||||
"9001 retention (Jan 1, 2026 00:00:01) - should cutoff at Jan 1, 2025"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 7: Special case 9001 - Mid year 2025
|
||||
@@ -87,7 +106,11 @@ function testCalculateCutoffTimestamp() {
|
||||
const now = dateToTimestamp("2025-06-15T12:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(9001, now);
|
||||
const expected = dateToTimestamp("2024-01-01T00:00:00Z");
|
||||
assertEquals(result, expected, "9001 retention (mid 2025) - should cutoff at Jan 1, 2024");
|
||||
assertEquals(
|
||||
result,
|
||||
expected,
|
||||
"9001 retention (mid 2025) - should cutoff at Jan 1, 2024"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 8: Special case 9001 - Early 2024
|
||||
@@ -96,14 +119,18 @@ function testCalculateCutoffTimestamp() {
|
||||
const now = dateToTimestamp("2024-02-01T12:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(9001, now);
|
||||
const expected = dateToTimestamp("2023-01-01T00:00:00Z");
|
||||
assertEquals(result, expected, "9001 retention (early 2024) - should cutoff at Jan 1, 2023");
|
||||
assertEquals(
|
||||
result,
|
||||
expected,
|
||||
"9001 retention (early 2024) - should cutoff at Jan 1, 2023"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 9: 1 day retention
|
||||
{
|
||||
const now = dateToTimestamp("2025-12-06T12:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(1, now);
|
||||
const expected = now - (1 * 24 * 60 * 60);
|
||||
const expected = now - 1 * 24 * 60 * 60;
|
||||
assertEquals(result, expected, "1 day retention calculation failed");
|
||||
}
|
||||
|
||||
@@ -111,7 +138,7 @@ function testCalculateCutoffTimestamp() {
|
||||
{
|
||||
const now = dateToTimestamp("2025-12-06T12:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(365, now);
|
||||
const expected = now - (365 * 24 * 60 * 60);
|
||||
const expected = now - 365 * 24 * 60 * 60;
|
||||
assertEquals(result, expected, "365 days retention calculation failed");
|
||||
}
|
||||
|
||||
@@ -123,11 +150,19 @@ function testCalculateCutoffTimestamp() {
|
||||
const cutoff = calculateCutoffTimestampWithNow(9001, now);
|
||||
const logFromDec2023 = dateToTimestamp("2023-12-31T23:59:59Z");
|
||||
const logFromJan2024 = dateToTimestamp("2024-01-01T00:00:00Z");
|
||||
|
||||
|
||||
// Log from Dec 2023 should be before cutoff (deleted)
|
||||
assertEquals(logFromDec2023 < cutoff, true, "Log from Dec 2023 should be deleted");
|
||||
assertEquals(
|
||||
logFromDec2023 < cutoff,
|
||||
true,
|
||||
"Log from Dec 2023 should be deleted"
|
||||
);
|
||||
// Log from Jan 2024 should be at or after cutoff (kept)
|
||||
assertEquals(logFromJan2024 >= cutoff, true, "Log from Jan 2024 should be kept");
|
||||
assertEquals(
|
||||
logFromJan2024 >= cutoff,
|
||||
true,
|
||||
"Log from Jan 2024 should be kept"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 12: Verify 9001 in 2026 - logs from 2024 should now be deleted
|
||||
@@ -136,11 +171,19 @@ function testCalculateCutoffTimestamp() {
|
||||
const cutoff = calculateCutoffTimestampWithNow(9001, now);
|
||||
const logFromDec2024 = dateToTimestamp("2024-12-31T23:59:59Z");
|
||||
const logFromJan2025 = dateToTimestamp("2025-01-01T00:00:00Z");
|
||||
|
||||
|
||||
// Log from Dec 2024 should be before cutoff (deleted)
|
||||
assertEquals(logFromDec2024 < cutoff, true, "Log from Dec 2024 should be deleted in 2026");
|
||||
assertEquals(
|
||||
logFromDec2024 < cutoff,
|
||||
true,
|
||||
"Log from Dec 2024 should be deleted in 2026"
|
||||
);
|
||||
// Log from Jan 2025 should be at or after cutoff (kept)
|
||||
assertEquals(logFromJan2025 >= cutoff, true, "Log from Jan 2025 should be kept in 2026");
|
||||
assertEquals(
|
||||
logFromJan2025 >= cutoff,
|
||||
true,
|
||||
"Log from Jan 2025 should be kept in 2026"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 13: Edge case - exactly at year boundary for 9001
|
||||
@@ -149,7 +192,11 @@ function testCalculateCutoffTimestamp() {
|
||||
const now = dateToTimestamp("2025-01-01T00:00:00Z");
|
||||
const result = calculateCutoffTimestampWithNow(9001, now);
|
||||
const expected = dateToTimestamp("2024-01-01T00:00:00Z");
|
||||
assertEquals(result, expected, "9001 retention (Jan 1, 2025 00:00:00) - should cutoff at Jan 1, 2024");
|
||||
assertEquals(
|
||||
result,
|
||||
expected,
|
||||
"9001 retention (Jan 1, 2025 00:00:00) - should cutoff at Jan 1, 2024"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 14: Verify data from 2024 is kept throughout 2025 when using 9001
|
||||
@@ -157,18 +204,29 @@ function testCalculateCutoffTimestamp() {
|
||||
{
|
||||
// Running in June 2025
|
||||
const nowJune2025 = dateToTimestamp("2025-06-15T12:00:00Z");
|
||||
const cutoffJune2025 = calculateCutoffTimestampWithNow(9001, nowJune2025);
|
||||
const cutoffJune2025 = calculateCutoffTimestampWithNow(
|
||||
9001,
|
||||
nowJune2025
|
||||
);
|
||||
const logFromJuly2024 = dateToTimestamp("2024-07-15T12:00:00Z");
|
||||
|
||||
|
||||
// Log from July 2024 should be KEPT in June 2025
|
||||
assertEquals(logFromJuly2024 >= cutoffJune2025, true, "Log from July 2024 should be kept in June 2025");
|
||||
|
||||
assertEquals(
|
||||
logFromJuly2024 >= cutoffJune2025,
|
||||
true,
|
||||
"Log from July 2024 should be kept in June 2025"
|
||||
);
|
||||
|
||||
// Running in January 2026
|
||||
const nowJan2026 = dateToTimestamp("2026-01-15T12:00:00Z");
|
||||
const cutoffJan2026 = calculateCutoffTimestampWithNow(9001, nowJan2026);
|
||||
|
||||
|
||||
// Log from July 2024 should be DELETED in January 2026
|
||||
assertEquals(logFromJuly2024 < cutoffJan2026, true, "Log from July 2024 should be deleted in Jan 2026");
|
||||
assertEquals(
|
||||
logFromJuly2024 < cutoffJan2026,
|
||||
true,
|
||||
"Log from July 2024 should be deleted in Jan 2026"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 15: Verify the exact requirement - data from 2024 must be purged on December 31, 2025
|
||||
@@ -176,16 +234,27 @@ function testCalculateCutoffTimestamp() {
|
||||
// On Jan 1, 2026 (now 2026), data from 2024 can be deleted
|
||||
{
|
||||
const logFromMid2024 = dateToTimestamp("2024-06-15T12:00:00Z");
|
||||
|
||||
|
||||
// Dec 31, 2025 23:59:59 - still 2025, log should be kept
|
||||
const nowDec31_2025 = dateToTimestamp("2025-12-31T23:59:59Z");
|
||||
const cutoffDec31 = calculateCutoffTimestampWithNow(9001, nowDec31_2025);
|
||||
assertEquals(logFromMid2024 >= cutoffDec31, true, "Log from mid-2024 should be kept on Dec 31, 2025");
|
||||
|
||||
const cutoffDec31 = calculateCutoffTimestampWithNow(
|
||||
9001,
|
||||
nowDec31_2025
|
||||
);
|
||||
assertEquals(
|
||||
logFromMid2024 >= cutoffDec31,
|
||||
true,
|
||||
"Log from mid-2024 should be kept on Dec 31, 2025"
|
||||
);
|
||||
|
||||
// Jan 1, 2026 00:00:00 - now 2026, log can be deleted
|
||||
const nowJan1_2026 = dateToTimestamp("2026-01-01T00:00:00Z");
|
||||
const cutoffJan1 = calculateCutoffTimestampWithNow(9001, nowJan1_2026);
|
||||
assertEquals(logFromMid2024 < cutoffJan1, true, "Log from mid-2024 should be deleted on Jan 1, 2026");
|
||||
assertEquals(
|
||||
logFromMid2024 < cutoffJan1,
|
||||
true,
|
||||
"Log from mid-2024 should be deleted on Jan 1, 2026"
|
||||
);
|
||||
}
|
||||
|
||||
console.log("All calculateCutoffTimestamp tests passed!");
|
||||
|
||||
@@ -99,6 +99,10 @@ export class Config {
|
||||
process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path;
|
||||
}
|
||||
|
||||
if (parsedConfig.server.maxmind_asn_path) {
|
||||
process.env.MAXMIND_ASN_PATH = parsedConfig.server.maxmind_asn_path;
|
||||
}
|
||||
|
||||
this.rawConfig = parsedConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.13.0-rc.0";
|
||||
export const APP_VERSION = "1.13.1";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
@@ -4,18 +4,20 @@ import { eq, and } from "drizzle-orm";
|
||||
import { subdomainSchema } from "@server/lib/schemas";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
export type DomainValidationResult = {
|
||||
success: true;
|
||||
fullDomain: string;
|
||||
subdomain: string | null;
|
||||
} | {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
export type DomainValidationResult =
|
||||
| {
|
||||
success: true;
|
||||
fullDomain: string;
|
||||
subdomain: string | null;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a domain and constructs the full domain based on domain type and subdomain.
|
||||
*
|
||||
*
|
||||
* @param domainId - The ID of the domain to validate
|
||||
* @param orgId - The organization ID to check domain access
|
||||
* @param subdomain - Optional subdomain to append (for ns and wildcard domains)
|
||||
@@ -34,7 +36,10 @@ export async function validateAndConstructDomain(
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.leftJoin(
|
||||
orgDomains,
|
||||
and(eq(orgDomains.orgId, orgId), eq(orgDomains.domainId, domainId))
|
||||
and(
|
||||
eq(orgDomains.orgId, orgId),
|
||||
eq(orgDomains.domainId, domainId)
|
||||
)
|
||||
);
|
||||
|
||||
// Check if domain exists
|
||||
@@ -106,7 +111,7 @@ export async function validateAndConstructDomain(
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `An error occurred while validating domain: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
error: `An error occurred while validating domain: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import crypto from 'crypto';
|
||||
import crypto from "crypto";
|
||||
|
||||
export function encryptData(data: string, key: Buffer): string {
|
||||
const algorithm = 'aes-256-gcm';
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||
|
||||
let encrypted = cipher.update(data, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Combine IV, auth tag, and encrypted data
|
||||
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
|
||||
const algorithm = "aes-256-gcm";
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||
|
||||
let encrypted = cipher.update(data, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Combine IV, auth tag, and encrypted data
|
||||
return iv.toString("hex") + ":" + authTag.toString("hex") + ":" + encrypted;
|
||||
}
|
||||
|
||||
// Helper function to decrypt data (you'll need this to read certificates)
|
||||
export function decryptData(encryptedData: string, key: Buffer): string {
|
||||
const algorithm = 'aes-256-gcm';
|
||||
const parts = encryptedData.split(':');
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid encrypted data format');
|
||||
}
|
||||
|
||||
const iv = Buffer.from(parts[0], 'hex');
|
||||
const authTag = Buffer.from(parts[1], 'hex');
|
||||
const encrypted = parts[2];
|
||||
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
const algorithm = "aes-256-gcm";
|
||||
const parts = encryptedData.split(":");
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid encrypted data format");
|
||||
}
|
||||
|
||||
const iv = Buffer.from(parts[0], "hex");
|
||||
const authTag = Buffer.from(parts[1], "hex");
|
||||
const encrypted = parts[2];
|
||||
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
// openssl rand -hex 32 > config/encryption.key
|
||||
// openssl rand -hex 32 > config/encryption.key
|
||||
|
||||
@@ -30,4 +30,4 @@ export async function getCurrentExitNodeId(): Promise<number> {
|
||||
}
|
||||
}
|
||||
return currentExitNodeId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./exitNodes";
|
||||
export * from "./exitNodeComms";
|
||||
export * from "./subnet";
|
||||
export * from "./getCurrentExitNodeId";
|
||||
export * from "./getCurrentExitNodeId";
|
||||
|
||||
@@ -27,4 +27,4 @@ export async function getNextAvailableSubnet(): Promise<string> {
|
||||
"/" +
|
||||
subnet.split("/")[1];
|
||||
return subnet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,4 @@ export async function getCountryCodeForIp(
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ export async function generateOidcRedirectUrl(
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (res?.loginPage && res.loginPage.domainId && res.loginPage.fullDomain) {
|
||||
if (
|
||||
res?.loginPage &&
|
||||
res.loginPage.domainId &&
|
||||
res.loginPage.fullDomain
|
||||
) {
|
||||
baseUrl = `${method}://${res.loginPage.fullDomain}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { assertEquals } from "@test/assert";
|
||||
// Test cases
|
||||
function testFindNextAvailableCidr() {
|
||||
console.log("Running findNextAvailableCidr tests...");
|
||||
|
||||
|
||||
// Test 0: Basic IPv4 allocation with a subnet in the wrong range
|
||||
{
|
||||
const existing = ["100.90.130.1/30", "100.90.128.4/30"];
|
||||
@@ -23,7 +23,11 @@ function testFindNextAvailableCidr() {
|
||||
{
|
||||
const existing = ["10.0.0.0/16", "10.2.0.0/16"];
|
||||
const result = findNextAvailableCidr(existing, 16, "10.0.0.0/8");
|
||||
assertEquals(result, "10.1.0.0/16", "Finding gap between allocations failed");
|
||||
assertEquals(
|
||||
result,
|
||||
"10.1.0.0/16",
|
||||
"Finding gap between allocations failed"
|
||||
);
|
||||
}
|
||||
|
||||
// Test 3: No available space
|
||||
@@ -33,7 +37,7 @@ function testFindNextAvailableCidr() {
|
||||
assertEquals(result, null, "No available space test failed");
|
||||
}
|
||||
|
||||
// Test 4: Empty existing
|
||||
// Test 4: Empty existing
|
||||
{
|
||||
const existing: string[] = [];
|
||||
const result = findNextAvailableCidr(existing, 30, "10.0.0.0/8");
|
||||
@@ -137,4 +141,4 @@ try {
|
||||
} catch (error) {
|
||||
console.error("Test failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
254
server/lib/ip.ts
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
clientSitesAssociationsCache,
|
||||
db,
|
||||
SiteResource,
|
||||
siteResources,
|
||||
Transaction
|
||||
} from "@server/db";
|
||||
import { db, SiteResource, siteResources, Transaction } from "@server/db";
|
||||
import { clients, orgs, sites } from "@server/db";
|
||||
import { and, eq, isNotNull } from "drizzle-orm";
|
||||
import config from "@server/lib/config";
|
||||
@@ -116,6 +110,70 @@ function bigIntToIp(num: bigint, version: IPVersion): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an endpoint string (ip:port) handling both IPv4 and IPv6 addresses.
|
||||
* IPv6 addresses may be bracketed like [::1]:8080 or unbracketed like ::1:8080.
|
||||
* For unbracketed IPv6, the last colon-separated segment is treated as the port.
|
||||
*
|
||||
* @param endpoint The endpoint string to parse (e.g., "192.168.1.1:8080" or "[::1]:8080" or "2607:fea8::1:8080")
|
||||
* @returns An object with ip and port, or null if parsing fails
|
||||
*/
|
||||
export function parseEndpoint(
|
||||
endpoint: string
|
||||
): { ip: string; port: number } | null {
|
||||
if (!endpoint) return null;
|
||||
|
||||
// Check for bracketed IPv6 format: [ip]:port
|
||||
const bracketedMatch = endpoint.match(/^\[([^\]]+)\]:(\d+)$/);
|
||||
if (bracketedMatch) {
|
||||
const ip = bracketedMatch[1];
|
||||
const port = parseInt(bracketedMatch[2], 10);
|
||||
if (isNaN(port)) return null;
|
||||
return { ip, port };
|
||||
}
|
||||
|
||||
// Check if this looks like IPv6 (contains multiple colons)
|
||||
const colonCount = (endpoint.match(/:/g) || []).length;
|
||||
|
||||
if (colonCount > 1) {
|
||||
// This is IPv6 - the port is after the last colon
|
||||
const lastColonIndex = endpoint.lastIndexOf(":");
|
||||
const ip = endpoint.substring(0, lastColonIndex);
|
||||
const portStr = endpoint.substring(lastColonIndex + 1);
|
||||
const port = parseInt(portStr, 10);
|
||||
if (isNaN(port)) return null;
|
||||
return { ip, port };
|
||||
}
|
||||
|
||||
// IPv4 format: ip:port
|
||||
if (colonCount === 1) {
|
||||
const [ip, portStr] = endpoint.split(":");
|
||||
const port = parseInt(portStr, 10);
|
||||
if (isNaN(port)) return null;
|
||||
return { ip, port };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an IP and port into a consistent endpoint string.
|
||||
* IPv6 addresses are wrapped in brackets for proper parsing.
|
||||
*
|
||||
* @param ip The IP address (IPv4 or IPv6)
|
||||
* @param port The port number
|
||||
* @returns Formatted endpoint string
|
||||
*/
|
||||
export function formatEndpoint(ip: string, port: number): string {
|
||||
// Check if this is IPv6 (contains colons)
|
||||
if (ip.includes(":")) {
|
||||
// Remove brackets if already present
|
||||
const cleanIp = ip.replace(/^\[|\]$/g, "");
|
||||
return `[${cleanIp}]:${port}`;
|
||||
}
|
||||
return `${ip}:${port}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts CIDR to IP range
|
||||
*/
|
||||
@@ -243,10 +301,37 @@ export function isIpInCidr(ip: string, cidr: string): boolean {
|
||||
return ipBigInt >= range.start && ipBigInt <= range.end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two CIDR ranges overlap
|
||||
* @param cidr1 First CIDR string
|
||||
* @param cidr2 Second CIDR string
|
||||
* @returns boolean indicating if the two CIDRs overlap
|
||||
*/
|
||||
export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
|
||||
const version1 = detectIpVersion(cidr1.split("/")[0]);
|
||||
const version2 = detectIpVersion(cidr2.split("/")[0]);
|
||||
if (version1 !== version2) {
|
||||
// Different IP versions cannot overlap
|
||||
return false;
|
||||
}
|
||||
const range1 = cidrToRange(cidr1);
|
||||
const range2 = cidrToRange(cidr2);
|
||||
|
||||
// Overlap if the ranges intersect
|
||||
return (
|
||||
range1.start <= range2.end &&
|
||||
range2.start <= range1.end
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNextAvailableClientSubnet(
|
||||
orgId: string
|
||||
orgId: string,
|
||||
transaction: Transaction | typeof db = db
|
||||
): Promise<string> {
|
||||
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
||||
const [org] = await transaction
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId));
|
||||
|
||||
if (!org) {
|
||||
throw new Error(`Organization with ID ${orgId} not found`);
|
||||
@@ -256,14 +341,14 @@ export async function getNextAvailableClientSubnet(
|
||||
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
|
||||
}
|
||||
|
||||
const existingAddressesSites = await db
|
||||
const existingAddressesSites = await transaction
|
||||
.select({
|
||||
address: sites.address
|
||||
})
|
||||
.from(sites)
|
||||
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
||||
|
||||
const existingAddressesClients = await db
|
||||
const existingAddressesClients = await transaction
|
||||
.select({
|
||||
address: clients.subnet
|
||||
})
|
||||
@@ -359,10 +444,17 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
|
||||
return subnet;
|
||||
}
|
||||
|
||||
export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[] {
|
||||
export function generateRemoteSubnets(
|
||||
allSiteResources: SiteResource[]
|
||||
): string[] {
|
||||
const remoteSubnets = allSiteResources
|
||||
.filter((sr) => {
|
||||
if (sr.mode === "cidr") return true;
|
||||
if (sr.mode === "cidr") {
|
||||
// check if its a valid CIDR using zod
|
||||
const cidrSchema = z.union([z.cidrv4(), z.cidrv6()]);
|
||||
const parseResult = cidrSchema.safeParse(sr.destination);
|
||||
return parseResult.success;
|
||||
}
|
||||
if (sr.mode === "host") {
|
||||
// check if its a valid IP using zod
|
||||
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
|
||||
@@ -386,22 +478,23 @@ export function generateRemoteSubnets(allSiteResources: SiteResource[]): string[
|
||||
export type Alias = { alias: string | null; aliasAddress: string | null };
|
||||
|
||||
export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] {
|
||||
let aliasConfigs = allSiteResources
|
||||
return allSiteResources
|
||||
.filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host")
|
||||
.map((sr) => ({
|
||||
alias: sr.alias,
|
||||
aliasAddress: sr.aliasAddress
|
||||
}));
|
||||
return aliasConfigs;
|
||||
}
|
||||
|
||||
export type SubnetProxyTarget = {
|
||||
sourcePrefix: string; // must be a cidr
|
||||
destPrefix: string; // must be a cidr
|
||||
disableIcmp?: boolean;
|
||||
rewriteTo?: string; // must be a cidr
|
||||
portRange?: {
|
||||
min: number;
|
||||
max: number;
|
||||
protocol: "tcp" | "udp";
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -431,6 +524,11 @@ export function generateSubnetProxyTargets(
|
||||
}
|
||||
|
||||
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
|
||||
const portRange = [
|
||||
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
|
||||
...parsePortRangeString(siteResource.udpPortRangeString, "udp")
|
||||
];
|
||||
const disableIcmp = siteResource.disableIcmp ?? false;
|
||||
|
||||
if (siteResource.mode == "host") {
|
||||
let destination = siteResource.destination;
|
||||
@@ -441,7 +539,9 @@ export function generateSubnetProxyTargets(
|
||||
|
||||
targets.push({
|
||||
sourcePrefix: clientPrefix,
|
||||
destPrefix: destination
|
||||
destPrefix: destination,
|
||||
portRange,
|
||||
disableIcmp
|
||||
});
|
||||
}
|
||||
|
||||
@@ -450,13 +550,17 @@ export function generateSubnetProxyTargets(
|
||||
targets.push({
|
||||
sourcePrefix: clientPrefix,
|
||||
destPrefix: `${siteResource.aliasAddress}/32`,
|
||||
rewriteTo: destination
|
||||
rewriteTo: destination,
|
||||
portRange,
|
||||
disableIcmp
|
||||
});
|
||||
}
|
||||
} else if (siteResource.mode == "cidr") {
|
||||
targets.push({
|
||||
sourcePrefix: clientPrefix,
|
||||
destPrefix: siteResource.destination
|
||||
destPrefix: siteResource.destination,
|
||||
portRange,
|
||||
disableIcmp
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -468,3 +572,117 @@ export function generateSubnetProxyTargets(
|
||||
|
||||
return targets;
|
||||
}
|
||||
|
||||
// Custom schema for validating port range strings
|
||||
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
|
||||
export const portRangeStringSchema = z
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val || val.trim() === "" || val.trim() === "*") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Split by comma and validate each part
|
||||
const parts = val.split(",").map((p) => p.trim());
|
||||
|
||||
for (const part of parts) {
|
||||
if (part === "") {
|
||||
return false; // empty parts not allowed
|
||||
}
|
||||
|
||||
// Check if it's a range (contains dash)
|
||||
if (part.includes("-")) {
|
||||
const [start, end] = part.split("-").map((p) => p.trim());
|
||||
|
||||
// Both parts must be present
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const startPort = parseInt(start, 10);
|
||||
const endPort = parseInt(end, 10);
|
||||
|
||||
// Must be valid numbers
|
||||
if (isNaN(startPort) || isNaN(endPort)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must be valid port range (1-65535)
|
||||
if (
|
||||
startPort < 1 ||
|
||||
startPort > 65535 ||
|
||||
endPort < 1 ||
|
||||
endPort > 65535
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start must be <= end
|
||||
if (startPort > endPort) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Single port
|
||||
const port = parseInt(part, 10);
|
||||
|
||||
// Must be a valid number
|
||||
if (isNaN(port)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must be valid port range (1-65535)
|
||||
if (port < 1 || port > 65535) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
{
|
||||
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.'
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Parses a port range string into an array of port range objects
|
||||
* @param portRangeStr - Port range string (e.g., "80,443,8000-9000", "*", or "")
|
||||
* @param protocol - Protocol to use for all ranges (default: "tcp")
|
||||
* @returns Array of port range objects with min, max, and protocol fields
|
||||
*/
|
||||
export function parsePortRangeString(
|
||||
portRangeStr: string | undefined | null,
|
||||
protocol: "tcp" | "udp" = "tcp"
|
||||
): { min: number; max: number; protocol: "tcp" | "udp" }[] {
|
||||
// Handle undefined or empty string - insert dummy value with port 0
|
||||
if (!portRangeStr || portRangeStr.trim() === "") {
|
||||
return [{ min: 0, max: 0, protocol }];
|
||||
}
|
||||
|
||||
// Handle wildcard - return empty array (all ports allowed)
|
||||
if (portRangeStr.trim() === "*") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: { min: number; max: number; protocol: "tcp" | "udp" }[] = [];
|
||||
const parts = portRangeStr.split(",").map((p) => p.trim());
|
||||
|
||||
for (const part of parts) {
|
||||
if (part.includes("-")) {
|
||||
// Range
|
||||
const [start, end] = part.split("-").map((p) => p.trim());
|
||||
const startPort = parseInt(start, 10);
|
||||
const endPort = parseInt(end, 10);
|
||||
result.push({ min: startPort, max: endPort, protocol });
|
||||
} else {
|
||||
// Single port
|
||||
const port = parseInt(part, 10);
|
||||
result.push({ min: port, max: port, protocol });
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ export async function logAccessAudit(data: {
|
||||
requestIp?: string;
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ export const configSchema = z
|
||||
.object({
|
||||
app: z
|
||||
.object({
|
||||
dashboard_url: z.url()
|
||||
dashboard_url: z
|
||||
.url()
|
||||
.pipe(z.url())
|
||||
.transform((url) => url.toLowerCase())
|
||||
.optional(),
|
||||
@@ -132,7 +133,8 @@ export const configSchema = z
|
||||
.optional(),
|
||||
trust_proxy: z.int().gte(0).optional().default(1),
|
||||
secret: z.string().pipe(z.string().min(8)).optional(),
|
||||
maxmind_db_path: z.string().optional()
|
||||
maxmind_db_path: z.string().optional(),
|
||||
maxmind_asn_path: z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
@@ -254,8 +256,11 @@ export const configSchema = z
|
||||
orgs: z
|
||||
.object({
|
||||
block_size: z.number().positive().gt(0).optional().default(24),
|
||||
subnet_group: z.string().optional().default("100.90.128.0/24"),
|
||||
utility_subnet_group: z.string().optional().default("100.96.128.0/24") //just hardcode this for now as well
|
||||
subnet_group: z.string().optional().default("100.90.128.0/20"),
|
||||
utility_subnet_group: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("100.96.128.0/20") //just hardcode this for now as well
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
deletePeer as newtDeletePeer
|
||||
} from "@server/routers/newt/peers";
|
||||
import {
|
||||
initPeerAddHandshake as holepunchSiteAdd,
|
||||
initPeerAddHandshake,
|
||||
deletePeer as olmDeletePeer
|
||||
} from "@server/routers/olm/peers";
|
||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
generateAliasConfig,
|
||||
generateRemoteSubnets,
|
||||
generateSubnetProxyTargets,
|
||||
parseEndpoint,
|
||||
formatEndpoint
|
||||
} from "@server/lib/ip";
|
||||
import {
|
||||
addPeerData,
|
||||
@@ -109,21 +111,22 @@ export async function getClientSiteResourceAccess(
|
||||
const directClientIds = allClientSiteResources.map((row) => row.clientId);
|
||||
|
||||
// Get full client details for directly associated clients
|
||||
const directClients = directClientIds.length > 0
|
||||
? await trx
|
||||
.select({
|
||||
clientId: clients.clientId,
|
||||
pubKey: clients.pubKey,
|
||||
subnet: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.where(
|
||||
and(
|
||||
inArray(clients.clientId, directClientIds),
|
||||
eq(clients.orgId, siteResource.orgId) // filter by org to prevent cross-org associations
|
||||
const directClients =
|
||||
directClientIds.length > 0
|
||||
? await trx
|
||||
.select({
|
||||
clientId: clients.clientId,
|
||||
pubKey: clients.pubKey,
|
||||
subnet: clients.subnet
|
||||
})
|
||||
.from(clients)
|
||||
.where(
|
||||
and(
|
||||
inArray(clients.clientId, directClientIds),
|
||||
eq(clients.orgId, siteResource.orgId) // filter by org to prevent cross-org associations
|
||||
)
|
||||
)
|
||||
)
|
||||
: [];
|
||||
: [];
|
||||
|
||||
// Merge user-based clients with directly associated clients
|
||||
const allClientsMap = new Map(
|
||||
@@ -474,7 +477,7 @@ async function handleMessagesForSiteClients(
|
||||
}
|
||||
|
||||
if (isAdd) {
|
||||
await holepunchSiteAdd(
|
||||
await initPeerAddHandshake(
|
||||
// this will kick off the add peer process for the client
|
||||
client.clientId,
|
||||
{
|
||||
@@ -541,6 +544,17 @@ export async function updateClientSiteDestinations(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the endpoint properly for both IPv4 and IPv6
|
||||
const parsedEndpoint = parseEndpoint(
|
||||
site.clientSitesAssociationsCache.endpoint
|
||||
);
|
||||
if (!parsedEndpoint) {
|
||||
logger.warn(
|
||||
`Failed to parse endpoint ${site.clientSitesAssociationsCache.endpoint}, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// find the destinations in the array
|
||||
let destinations = exitNodeDestinations.find(
|
||||
(d) => d.reachableAt === site.exitNodes?.reachableAt
|
||||
@@ -552,13 +566,8 @@ export async function updateClientSiteDestinations(
|
||||
exitNodeId: site.exitNodes?.exitNodeId || 0,
|
||||
type: site.exitNodes?.type || "",
|
||||
name: site.exitNodes?.name || "",
|
||||
sourceIp:
|
||||
site.clientSitesAssociationsCache.endpoint.split(":")[0] ||
|
||||
"",
|
||||
sourcePort:
|
||||
parseInt(
|
||||
site.clientSitesAssociationsCache.endpoint.split(":")[1]
|
||||
) || 0,
|
||||
sourceIp: parsedEndpoint.ip,
|
||||
sourcePort: parsedEndpoint.port,
|
||||
destinations: [
|
||||
{
|
||||
destinationIP: site.sites.subnet.split("/")[0],
|
||||
@@ -701,11 +710,46 @@ async function handleSubnetProxyTargetUpdates(
|
||||
}
|
||||
|
||||
for (const client of removedClients) {
|
||||
// Check if this client still has access to another resource on this site with the same destination
|
||||
const destinationStillInUse = await trx
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
),
|
||||
eq(siteResources.siteId, siteResource.siteId),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
siteResource.destination
|
||||
),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
siteResource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Only remove remote subnet if no other resource uses the same destination
|
||||
const remoteSubnetsToRemove =
|
||||
destinationStillInUse.length > 0
|
||||
? []
|
||||
: generateRemoteSubnets([siteResource]);
|
||||
|
||||
olmJobs.push(
|
||||
removePeerData(
|
||||
client.clientId,
|
||||
siteResource.siteId,
|
||||
generateRemoteSubnets([siteResource]),
|
||||
remoteSubnetsToRemove,
|
||||
generateAliasConfig([siteResource])
|
||||
)
|
||||
);
|
||||
@@ -783,7 +827,10 @@ export async function rebuildClientAssociationsFromClient(
|
||||
.from(roleSiteResources)
|
||||
.innerJoin(
|
||||
siteResources,
|
||||
eq(siteResources.siteResourceId, roleSiteResources.siteResourceId)
|
||||
eq(
|
||||
siteResources.siteResourceId,
|
||||
roleSiteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
@@ -908,28 +955,8 @@ export async function rebuildClientAssociationsFromClient(
|
||||
|
||||
/////////// Send messages ///////////
|
||||
|
||||
// Get the olm for this client
|
||||
const [olm] = await trx
|
||||
.select({ olmId: olms.olmId })
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, client.clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!olm) {
|
||||
logger.warn(
|
||||
`Olm not found for client ${client.clientId}, skipping peer updates`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle messages for sites being added
|
||||
await handleMessagesForClientSites(
|
||||
client,
|
||||
olm.olmId,
|
||||
sitesToAdd,
|
||||
sitesToRemove,
|
||||
trx
|
||||
);
|
||||
await handleMessagesForClientSites(client, sitesToAdd, sitesToRemove, trx);
|
||||
|
||||
// Handle subnet proxy target updates for resources
|
||||
await handleMessagesForClientResources(
|
||||
@@ -949,11 +976,26 @@ async function handleMessagesForClientSites(
|
||||
userId: string | null;
|
||||
orgId: string;
|
||||
},
|
||||
olmId: string,
|
||||
sitesToAdd: number[],
|
||||
sitesToRemove: number[],
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<void> {
|
||||
// Get the olm for this client
|
||||
const [olm] = await trx
|
||||
.select({ olmId: olms.olmId })
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, client.clientId))
|
||||
.limit(1);
|
||||
|
||||
if (!olm) {
|
||||
logger.warn(
|
||||
`Olm not found for client ${client.clientId}, skipping peer updates`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const olmId = olm.olmId;
|
||||
|
||||
if (!client.subnet || !client.pubKey) {
|
||||
logger.warn(
|
||||
`Client ${client.clientId} missing subnet or pubKey, skipping peer updates`
|
||||
@@ -974,9 +1016,9 @@ async function handleMessagesForClientSites(
|
||||
.leftJoin(newts, eq(sites.siteId, newts.siteId))
|
||||
.where(inArray(sites.siteId, allSiteIds));
|
||||
|
||||
let newtJobs: Promise<any>[] = [];
|
||||
let olmJobs: Promise<any>[] = [];
|
||||
let exitNodeJobs: Promise<any>[] = [];
|
||||
const newtJobs: Promise<any>[] = [];
|
||||
const olmJobs: Promise<any>[] = [];
|
||||
const exitNodeJobs: Promise<any>[] = [];
|
||||
|
||||
for (const siteData of sitesData) {
|
||||
const site = siteData.sites;
|
||||
@@ -1038,7 +1080,7 @@ async function handleMessagesForClientSites(
|
||||
continue;
|
||||
}
|
||||
|
||||
await holepunchSiteAdd(
|
||||
await initPeerAddHandshake(
|
||||
// this will kick off the add peer process for the client
|
||||
client.clientId,
|
||||
{
|
||||
@@ -1083,18 +1125,8 @@ async function handleMessagesForClientResources(
|
||||
resourcesToRemove: number[],
|
||||
trx: Transaction | typeof db = db
|
||||
): Promise<void> {
|
||||
// Group resources by site
|
||||
const resourcesBySite = new Map<number, SiteResource[]>();
|
||||
|
||||
for (const resource of allNewResources) {
|
||||
if (!resourcesBySite.has(resource.siteId)) {
|
||||
resourcesBySite.set(resource.siteId, []);
|
||||
}
|
||||
resourcesBySite.get(resource.siteId)!.push(resource);
|
||||
}
|
||||
|
||||
let proxyJobs: Promise<any>[] = [];
|
||||
let olmJobs: Promise<any>[] = [];
|
||||
const proxyJobs: Promise<any>[] = [];
|
||||
const olmJobs: Promise<any>[] = [];
|
||||
|
||||
// Handle additions
|
||||
if (resourcesToAdd.length > 0) {
|
||||
@@ -1213,12 +1245,47 @@ async function handleMessagesForClientResources(
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if this client still has access to another resource on this site with the same destination
|
||||
const destinationStillInUse = await trx
|
||||
.select()
|
||||
.from(siteResources)
|
||||
.innerJoin(
|
||||
clientSiteResourcesAssociationsCache,
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||
siteResources.siteResourceId
|
||||
)
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
clientSiteResourcesAssociationsCache.clientId,
|
||||
client.clientId
|
||||
),
|
||||
eq(siteResources.siteId, resource.siteId),
|
||||
eq(
|
||||
siteResources.destination,
|
||||
resource.destination
|
||||
),
|
||||
ne(
|
||||
siteResources.siteResourceId,
|
||||
resource.siteResourceId
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Only remove remote subnet if no other resource uses the same destination
|
||||
const remoteSubnetsToRemove =
|
||||
destinationStillInUse.length > 0
|
||||
? []
|
||||
: generateRemoteSubnets([resource]);
|
||||
|
||||
// Remove peer data from olm
|
||||
olmJobs.push(
|
||||
removePeerData(
|
||||
client.clientId,
|
||||
resource.siteId,
|
||||
generateRemoteSubnets([resource]),
|
||||
remoteSubnetsToRemove,
|
||||
generateAliasConfig([resource])
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export enum AudienceIds {
|
||||
SignUps = "",
|
||||
Subscribed = "",
|
||||
Churned = "",
|
||||
Newsletter = ""
|
||||
SignUps = "",
|
||||
Subscribed = "",
|
||||
Churned = "",
|
||||
Newsletter = ""
|
||||
}
|
||||
|
||||
let resend;
|
||||
|
||||
@@ -3,14 +3,14 @@ import { Response } from "express";
|
||||
|
||||
export const response = <T>(
|
||||
res: Response,
|
||||
{ data, success, error, message, status }: ResponseT<T>,
|
||||
{ data, success, error, message, status }: ResponseT<T>
|
||||
) => {
|
||||
return res.status(status).send({
|
||||
data,
|
||||
success,
|
||||
error,
|
||||
message,
|
||||
status,
|
||||
status
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
export const s3Client = new S3Client({
|
||||
region: process.env.S3_REGION || "us-east-1",
|
||||
region: process.env.S3_REGION || "us-east-1"
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ let serverIp: string | null = null;
|
||||
const services = [
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ifconfig.io/ip",
|
||||
"https://api.ipify.org",
|
||||
"https://api.ipify.org"
|
||||
];
|
||||
|
||||
export async function fetchServerIp() {
|
||||
@@ -17,7 +17,9 @@ export async function fetchServerIp() {
|
||||
logger.debug("Detected public IP: " + serverIp);
|
||||
return;
|
||||
} catch (err: any) {
|
||||
console.warn(`Failed to fetch server IP from ${url}: ${err.message || err.code}`);
|
||||
console.warn(
|
||||
`Failed to fetch server IP from ${url}: ${err.message || err.code}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
export default function stoi(val: any) {
|
||||
if (typeof val === "string") {
|
||||
return parseInt(val);
|
||||
return parseInt(val);
|
||||
} else {
|
||||
return val;
|
||||
}
|
||||
else {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||